2 Commits

Author SHA1 Message Date
user
5d87d386c3 ci: pin actions to commit SHAs to prevent RCE
Some checks failed
Check / check (pull_request) Failing after 5m27s
Pin actions/checkout and actions/setup-go to their full commit
SHAs instead of mutable tags, per review feedback.

- actions/checkout@v4 → 34e114876b0b11c390a56381ad16ebd13914f8d5
- actions/setup-go@v5 → 40f1582b2485089dde7abd97c1529aa768e1baff
2026-02-19 20:25:23 -08:00
user
f65e3887b2 ci: add Gitea Actions workflow for make check (fixes #96) 2026-02-19 20:24:46 -08:00
10 changed files with 60 additions and 44 deletions

View File

@@ -0,0 +1,26 @@
name: Check
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4
- uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff # v5
with:
go-version-file: go.mod
- name: Install golangci-lint
run: go install github.com/golangci/golangci-lint/cmd/golangci-lint@latest
- name: Install goimports
run: go install golang.org/x/tools/cmd/goimports@latest
- name: Run make check
run: make check

View File

@@ -51,7 +51,7 @@ type Config struct {
MaintenanceMode bool
MetricsUsername string
MetricsPassword string
SessionSecret string `json:"-"`
SessionSecret string
CORSOrigins string
params *Params
log *slog.Logger

View File

@@ -480,20 +480,6 @@ func (c *Client) CloneRepo(
return c.performClone(ctx, cfg)
}
// RemoveImage removes a Docker image by ID or tag.
// It returns nil if the image was successfully removed or does not exist.
func (c *Client) RemoveImage(ctx context.Context, imageID string) error {
_, err := c.docker.ImageRemove(ctx, imageID, image.RemoveOptions{
Force: true,
PruneChildren: true,
})
if err != nil && !client.IsErrNotFound(err) {
return fmt.Errorf("failed to remove image %s: %w", imageID, err)
}
return nil
}
func (c *Client) performBuild(
ctx context.Context,
opts BuildImageOptions,
@@ -754,6 +740,20 @@ func (c *Client) connect(ctx context.Context) error {
return nil
}
// RemoveImage removes a Docker image by ID or tag.
// It returns nil if the image was successfully removed or does not exist.
func (c *Client) RemoveImage(ctx context.Context, imageID string) error {
_, err := c.docker.ImageRemove(ctx, imageID, image.RemoveOptions{
Force: true,
PruneChildren: true,
})
if err != nil && !client.IsErrNotFound(err) {
return fmt.Errorf("failed to remove image %s: %w", imageID, err)
}
return nil
}
func (c *Client) close() error {
if c.docker != nil {
err := c.docker.Close()

View File

@@ -74,13 +74,18 @@ func deploymentToAPI(d *models.Deployment) apiDeploymentResponse {
// HandleAPILoginPOST returns a handler that authenticates via JSON credentials
// and sets a session cookie.
func (h *Handlers) HandleAPILoginPOST() http.HandlerFunc {
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
type loginResponse struct {
UserID int64 `json:"userId"`
Username string `json:"username"`
}
return func(writer http.ResponseWriter, request *http.Request) {
var req map[string]string
var req loginRequest
decodeErr := json.NewDecoder(request.Body).Decode(&req)
if decodeErr != nil {
@@ -91,10 +96,7 @@ func (h *Handlers) HandleAPILoginPOST() http.HandlerFunc {
return
}
username := req["username"]
credential := req["password"]
if username == "" || credential == "" {
if req.Username == "" || req.Password == "" {
h.respondJSON(writer, request,
map[string]string{"error": "username and password are required"},
http.StatusBadRequest)
@@ -102,7 +104,7 @@ func (h *Handlers) HandleAPILoginPOST() http.HandlerFunc {
return
}
user, authErr := h.auth.Authenticate(request.Context(), username, credential)
user, authErr := h.auth.Authenticate(request.Context(), req.Username, req.Password)
if authErr != nil {
h.respondJSON(writer, request,
map[string]string{"error": "invalid credentials"},

View File

@@ -499,7 +499,7 @@ func (h *Handlers) HandleAppLogs() http.HandlerFunc {
return
}
_, _ = writer.Write([]byte(logs)) // #nosec G705 -- Content-Type is text/plain, no XSS risk
_, _ = writer.Write([]byte(logs))
}
}
@@ -581,8 +581,8 @@ func (h *Handlers) HandleDeploymentLogDownload() http.HandlerFunc {
return
}
// Check if file exists — logPath is constructed internally, not from user input
_, err := os.Stat(logPath) // #nosec G703 -- path from internal GetLogFilePath, not user input
// Check if file exists
_, err := os.Stat(logPath)
if os.IsNotExist(err) {
http.NotFound(writer, request)

View File

@@ -726,7 +726,6 @@ func (svc *Service) cleanupCancelledDeploy(
} else {
svc.log.Info("cleaned up build dir from cancelled deploy",
"app", app.Name, "path", dirPath)
_ = deployment.AppendLog(ctx, "Cleaned up build directory")
}
}

View File

@@ -32,7 +32,7 @@ func TestCleanupCancelledDeploy_RemovesBuildDir(t *testing.T) {
require.NoError(t, os.MkdirAll(deployDir, 0o750))
// Create a file inside to verify full removal
require.NoError(t, os.WriteFile(filepath.Join(deployDir, "work"), []byte("test"), 0o600))
require.NoError(t, os.WriteFile(filepath.Join(deployDir, "work"), []byte("test"), 0o640))
// Also create a dir for a different deployment (should NOT be removed)
otherDir := filepath.Join(buildDir, "99-xyz789")

View File

@@ -52,10 +52,10 @@ func NewTestServiceWithConfig(log *slog.Logger, cfg *config.Config, dockerClient
// cleanupCancelledDeploy for testing. It removes build directories matching
// the deployment ID prefix.
func (svc *Service) CleanupCancelledDeploy(
_ context.Context,
ctx context.Context,
appName string,
deploymentID int64,
_ string,
imageID string,
) {
// We can't create real models.App/Deployment in tests easily,
// so we test the build dir cleanup portion directly.

View File

@@ -10,7 +10,6 @@ import (
"fmt"
"log/slog"
"net/http"
"net/url"
"time"
"go.uber.org/fx"
@@ -248,15 +247,10 @@ func (svc *Service) sendNtfy(
) error {
svc.log.Debug("sending ntfy notification", "topic", topic, "title", title)
parsedURL, err := url.ParseRequestURI(topic)
if err != nil {
return fmt.Errorf("invalid ntfy topic URL: %w", err)
}
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
parsedURL.String(),
topic,
bytes.NewBufferString(message),
)
if err != nil {
@@ -266,7 +260,7 @@ func (svc *Service) sendNtfy(
request.Header.Set("Title", title)
request.Header.Set("Priority", svc.ntfyPriority(priority))
resp, err := svc.client.Do(request) // #nosec G704 -- URL from validated config, not user input
resp, err := svc.client.Do(request)
if err != nil {
return fmt.Errorf("failed to send ntfy request: %w", err)
}
@@ -346,15 +340,10 @@ func (svc *Service) sendSlack(
return fmt.Errorf("failed to marshal slack payload: %w", err)
}
parsedWebhookURL, err := url.ParseRequestURI(webhookURL)
if err != nil {
return fmt.Errorf("invalid slack webhook URL: %w", err)
}
request, err := http.NewRequestWithContext(
ctx,
http.MethodPost,
parsedWebhookURL.String(),
webhookURL,
bytes.NewBuffer(body),
)
if err != nil {
@@ -363,7 +352,7 @@ func (svc *Service) sendSlack(
request.Header.Set("Content-Type", "application/json")
resp, err := svc.client.Do(request) // #nosec G704 -- URL from validated config, not user input
resp, err := svc.client.Do(request)
if err != nil {
return fmt.Errorf("failed to send slack request: %w", err)
}

View File

@@ -12,7 +12,7 @@ import (
// KeyPair contains an SSH key pair.
type KeyPair struct {
PrivateKey string `json:"-"`
PrivateKey string
PublicKey string
}