From 3e2aaee1fa58eb67f48b1dbba7b63665e004d4e9 Mon Sep 17 00:00:00 2001 From: Facundo Farias Date: Thu, 21 May 2026 11:33:47 +0200 Subject: [PATCH 1/2] feat(deployment-checks): add CRUD commands for deployment checks Wire the /projects/{id}/deployment_checks API into the SDK and CLI: ssh, http, and vulnerability_scan checks gating pre_build or post_deploy stages. Update sends only flags the user set (cmd.Flags().Changed) so partial updates don't clobber existing values. Co-Authored-By: Claude Opus 4.7 (1M context) --- internal/commands/agent_metadata.go | 21 ++ internal/commands/deployment_checks.go | 257 +++++++++++++++++++++++++ internal/commands/root.go | 1 + pkg/sdk/deployment_checks.go | 99 ++++++++++ pkg/sdk/deployment_checks_test.go | 121 ++++++++++++ 5 files changed, 499 insertions(+) create mode 100644 internal/commands/deployment_checks.go create mode 100644 pkg/sdk/deployment_checks.go create mode 100644 pkg/sdk/deployment_checks_test.go diff --git a/internal/commands/agent_metadata.go b/internal/commands/agent_metadata.go index 0841fec..3f952c6 100644 --- a/internal/commands/agent_metadata.go +++ b/internal/commands/agent_metadata.go @@ -211,6 +211,27 @@ var commandMetadataTable = map[string]AgentMetadata{ SupportsJSON: true, SafeForAutomation: true, ResourceTypes: []string{"ssh_command"}, }, + "dhq deployment-checks list": { + Idempotent: true, SupportsJSON: true, SafeForAutomation: true, + ResourceTypes: []string{"deployment_check"}, + }, + "dhq deployment-checks show": { + Idempotent: true, SupportsJSON: true, SafeForAutomation: true, + ResourceTypes: []string{"deployment_check"}, + }, + "dhq deployment-checks create": { + SupportsJSON: true, SafeForAutomation: true, + ResourceTypes: []string{"deployment_check"}, + }, + "dhq deployment-checks update": { + Idempotent: true, SupportsJSON: true, SafeForAutomation: true, + ResourceTypes: []string{"deployment_check"}, + }, + "dhq deployment-checks delete": { + Destructive: true, RequiresConfirmation: true, + SupportsJSON: true, SafeForAutomation: true, + ResourceTypes: []string{"deployment_check"}, + }, // Repos "dhq repos show": { diff --git a/internal/commands/deployment_checks.go b/internal/commands/deployment_checks.go new file mode 100644 index 0000000..fd6fce8 --- /dev/null +++ b/internal/commands/deployment_checks.go @@ -0,0 +1,257 @@ +package commands + +import ( + "fmt" + + "github.com/deployhq/deployhq-cli/internal/output" + "github.com/deployhq/deployhq-cli/pkg/sdk" + "github.com/spf13/cobra" +) + +func newDeploymentChecksCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "deployment-checks", + Short: "Manage deployment checks", + Long: `Deployment checks gate a deployment at one of two stages: pre_build (runs on the build server before the build) or post_deploy (runs after files have been uploaded). + +Three check types are supported: + ssh — runs a command over SSH on selected servers + http — sends an HTTP request from the deployment worker + vulnerability_scan — runs a security scanner (Snyk, Trivy, or a custom CLI emitting SARIF); pre_build only`, + } + cmd.AddCommand( + &cobra.Command{ + Use: "list", Short: "List deployment checks", + RunE: func(cmd *cobra.Command, args []string) error { + projectID, err := cliCtx.RequireProject() + if err != nil { + return err + } + client, err := cliCtx.Client() + if err != nil { + return err + } + checks, err := client.ListDeploymentChecks(cliCtx.Background(), projectID, nil) + if err != nil { + return err + } + env := cliCtx.Envelope + if env.JSONMode || !env.IsTTY { + return env.WriteJSON(output.NewResponse(checks, fmt.Sprintf("%d deployment checks", len(checks)))) + } + rows := make([][]string, len(checks)) + for i, c := range checks { + rows[i] = []string{c.Identifier, c.Name, c.Stage, c.CheckType, enabledLabel(c.Enabled)} + } + env.WriteTable([]string{"Identifier", "Name", "Stage", "Type", "Enabled"}, rows) + return nil + }, + }, + &cobra.Command{ + Use: "show ", Short: "Show deployment check details", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + projectID, err := cliCtx.RequireProject() + if err != nil { + return err + } + client, err := cliCtx.Client() + if err != nil { + return err + } + c, err := client.GetDeploymentCheck(cliCtx.Background(), projectID, args[0]) + if err != nil { + return err + } + return cliCtx.Envelope.WriteJSON(output.NewResponse(c, c.Name)) + }, + }, + newDeploymentChecksCreateCmd(), + newDeploymentChecksUpdateCmd(), + &cobra.Command{ + Use: "delete ", Short: "Delete a deployment check", Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + projectID, err := cliCtx.RequireProject() + if err != nil { + return err + } + client, err := cliCtx.Client() + if err != nil { + return err + } + if err := client.DeleteDeploymentCheck(cliCtx.Background(), projectID, args[0]); err != nil { + return err + } + cliCtx.Envelope.Status("Deleted deployment check: %s", args[0]) + return nil + }, + }, + ) + return cmd +} + +// checkFlags holds the shared flag set for create and update. +type checkFlags struct { + name, description, stage, checkType, command string + servers []string + httpMethod, httpURL, httpBodyMatch string + httpExpectedStatus, timeoutSeconds int + httpExpectedStatusSet, timeoutSecondsSet bool + scanner, scanTargetKind, scanTarget, severityThreshold string + sarifOutputPath string + enabled, failOnUnfixedOnly bool + enabledSet, failOnUnfixedOnlySet bool +} + +func (f *checkFlags) register(cmd *cobra.Command) { + cmd.Flags().StringVar(&f.name, "name", "", "Display name for the check") + cmd.Flags().StringVar(&f.description, "description", "", "Description") + cmd.Flags().StringVar(&f.stage, "stage", "", "Stage: pre_build or post_deploy") + cmd.Flags().StringVar(&f.checkType, "check-type", "", "Check type: ssh, http, or vulnerability_scan") + cmd.Flags().BoolVar(&f.enabled, "enabled", true, "Whether the check is enabled") + cmd.Flags().IntVar(&f.timeoutSeconds, "timeout", 0, "Timeout in seconds") + cmd.Flags().StringVar(&f.command, "command", "", "Command to run (ssh checks)") + cmd.Flags().StringSliceVar(&f.servers, "servers", nil, "Server identifiers to target (ssh checks); repeat or comma-separate") + cmd.Flags().StringVar(&f.httpMethod, "http-method", "", "HTTP method (http checks)") + cmd.Flags().StringVar(&f.httpURL, "http-url", "", "URL to request (http checks)") + cmd.Flags().IntVar(&f.httpExpectedStatus, "http-expected-status", 0, "Expected HTTP status code (http checks)") + cmd.Flags().StringVar(&f.httpBodyMatch, "http-body-match", "", "Substring expected in HTTP response body (http checks)") + cmd.Flags().StringVar(&f.scanner, "scanner", "", "Scanner: snyk, trivy, or custom (vulnerability_scan)") + cmd.Flags().StringVar(&f.scanTargetKind, "scan-target-kind", "", "Scan target kind (vulnerability_scan)") + cmd.Flags().StringVar(&f.scanTarget, "scan-target", "", "Scan target path or identifier (vulnerability_scan)") + cmd.Flags().StringVar(&f.severityThreshold, "severity-threshold", "", "Minimum severity that fails the check (vulnerability_scan)") + cmd.Flags().BoolVar(&f.failOnUnfixedOnly, "fail-on-unfixed-only", false, "Only fail on findings with no available fix (vulnerability_scan)") + cmd.Flags().StringVar(&f.sarifOutputPath, "sarif-output-path", "", "Path where the scanner writes SARIF output (vulnerability_scan)") +} + +// captureChanged inspects which flags the user actually set so omitted flags +// don't overwrite existing values on update. +func (f *checkFlags) captureChanged(cmd *cobra.Command) { + f.enabledSet = cmd.Flags().Changed("enabled") + f.timeoutSecondsSet = cmd.Flags().Changed("timeout") + f.httpExpectedStatusSet = cmd.Flags().Changed("http-expected-status") + f.failOnUnfixedOnlySet = cmd.Flags().Changed("fail-on-unfixed-only") +} + +func (f *checkFlags) toRequest() sdk.DeploymentCheckCreateRequest { + req := sdk.DeploymentCheckCreateRequest{ + Name: f.name, + Description: f.description, + Stage: f.stage, + CheckType: f.checkType, + Command: f.command, + Servers: f.servers, + HTTPMethod: f.httpMethod, + HTTPURL: f.httpURL, + HTTPBodyMatch: f.httpBodyMatch, + Scanner: f.scanner, + ScanTargetKind: f.scanTargetKind, + ScanTarget: f.scanTarget, + SeverityThreshold: f.severityThreshold, + SARIFOutputPath: f.sarifOutputPath, + } + if f.enabledSet { + enabled := f.enabled + req.Enabled = &enabled + } + if f.timeoutSecondsSet { + t := f.timeoutSeconds + req.TimeoutSeconds = &t + } + if f.httpExpectedStatusSet { + s := f.httpExpectedStatus + req.HTTPExpectedStatus = &s + } + if f.failOnUnfixedOnlySet { + fou := f.failOnUnfixedOnly + req.FailOnUnfixedOnly = &fou + } + return req +} + +func newDeploymentChecksCreateCmd() *cobra.Command { + f := &checkFlags{} + cmd := &cobra.Command{ + Use: "create", + Short: "Create a deployment check", + RunE: func(cmd *cobra.Command, args []string) error { + if f.name == "" { + return &output.UserError{Message: "--name is required"} + } + if f.stage == "" { + return &output.UserError{Message: "--stage is required (pre_build or post_deploy)"} + } + if f.checkType == "" { + return &output.UserError{Message: "--check-type is required (ssh, http, or vulnerability_scan)"} + } + switch f.checkType { + case "ssh": + if f.command == "" { + return &output.UserError{Message: "--command is required for ssh checks"} + } + case "http": + if f.httpURL == "" { + return &output.UserError{Message: "--http-url is required for http checks"} + } + case "vulnerability_scan": + if f.stage != "pre_build" { + return &output.UserError{Message: "vulnerability_scan checks must use --stage pre_build"} + } + if f.scanner == "" { + return &output.UserError{Message: "--scanner is required for vulnerability_scan checks"} + } + } + projectID, err := cliCtx.RequireProject() + if err != nil { + return err + } + client, err := cliCtx.Client() + if err != nil { + return err + } + f.captureChanged(cmd) + c, err := client.CreateDeploymentCheck(cliCtx.Background(), projectID, f.toRequest()) + if err != nil { + return err + } + cliCtx.Envelope.Status("Created deployment check: %s (%s)", c.Name, c.Identifier) + return nil + }, + } + f.register(cmd) + return cmd +} + +func newDeploymentChecksUpdateCmd() *cobra.Command { + f := &checkFlags{} + cmd := &cobra.Command{ + Use: "update ", + Short: "Update a deployment check", + Args: cobra.ExactArgs(1), + RunE: func(cmd *cobra.Command, args []string) error { + projectID, err := cliCtx.RequireProject() + if err != nil { + return err + } + client, err := cliCtx.Client() + if err != nil { + return err + } + f.captureChanged(cmd) + c, err := client.UpdateDeploymentCheck(cliCtx.Background(), projectID, args[0], f.toRequest()) + if err != nil { + return err + } + cliCtx.Envelope.Status("Updated deployment check: %s", c.Identifier) + return nil + }, + } + f.register(cmd) + return cmd +} + +func enabledLabel(enabled bool) string { + if enabled { + return "yes" + } + return "no" +} diff --git a/internal/commands/root.go b/internal/commands/root.go index 278a315..031bb29 100644 --- a/internal/commands/root.go +++ b/internal/commands/root.go @@ -174,6 +174,7 @@ Support: support@deployhq.com`, newBuildConfigsCmd(), newLanguageVersionsCmd(), newSSHCommandsCmd(), + newDeploymentChecksCmd(), newExcludedFilesCmd(), newIntegrationsCmd(), newAgentsCmd(), diff --git a/pkg/sdk/deployment_checks.go b/pkg/sdk/deployment_checks.go new file mode 100644 index 0000000..e957155 --- /dev/null +++ b/pkg/sdk/deployment_checks.go @@ -0,0 +1,99 @@ +package sdk + +import ( + "context" + "fmt" +) + +// DeploymentCheck represents a deployment check configured on a project. +// A check has a stage (pre_build or post_deploy) and a check_type (ssh, http, +// or vulnerability_scan). +type DeploymentCheck struct { + Identifier string `json:"identifier"` + Name string `json:"name"` + Description string `json:"description,omitempty"` + Stage string `json:"stage"` + CheckType string `json:"check_type"` + Enabled bool `json:"enabled"` + Position int `json:"position"` + TimeoutSeconds int `json:"timeout_seconds,omitempty"` + Command string `json:"command,omitempty"` + Servers []interface{} `json:"servers,omitempty"` + HTTPMethod string `json:"http_method,omitempty"` + HTTPURL string `json:"http_url,omitempty"` + HTTPExpectedStatus int `json:"http_expected_status,omitempty"` + HTTPBodyMatch string `json:"http_body_match,omitempty"` + Scanner string `json:"scanner,omitempty"` + ScanTargetKind string `json:"scan_target_kind,omitempty"` + ScanTarget string `json:"scan_target,omitempty"` + SeverityThreshold string `json:"severity_threshold,omitempty"` + FailOnUnfixedOnly bool `json:"fail_on_unfixed_only,omitempty"` + SARIFOutputPath string `json:"sarif_output_path,omitempty"` +} + +// DeploymentCheckCreateRequest is the payload for creating or updating a deployment check. +// Pointer fields are omitted from the JSON body when nil, so callers only send what they +// want to set (important for partial updates via PATCH). +type DeploymentCheckCreateRequest struct { + Name string `json:"name,omitempty"` + Description string `json:"description,omitempty"` + Stage string `json:"stage,omitempty"` + CheckType string `json:"check_type,omitempty"` + Enabled *bool `json:"enabled,omitempty"` + TimeoutSeconds *int `json:"timeout_seconds,omitempty"` + Command string `json:"command,omitempty"` + Servers []string `json:"servers,omitempty"` + HTTPMethod string `json:"http_method,omitempty"` + HTTPURL string `json:"http_url,omitempty"` + HTTPExpectedStatus *int `json:"http_expected_status,omitempty"` + HTTPBodyMatch string `json:"http_body_match,omitempty"` + Scanner string `json:"scanner,omitempty"` + ScanTargetKind string `json:"scan_target_kind,omitempty"` + ScanTarget string `json:"scan_target,omitempty"` + SeverityThreshold string `json:"severity_threshold,omitempty"` + FailOnUnfixedOnly *bool `json:"fail_on_unfixed_only,omitempty"` + SARIFOutputPath string `json:"sarif_output_path,omitempty"` +} + +func (c *Client) ListDeploymentChecks(ctx context.Context, projectID string, opts *ListOptions) ([]DeploymentCheck, error) { + var checks []DeploymentCheck + path := appendListParams(fmt.Sprintf("/projects/%s/deployment_checks", projectID), opts) + if err := c.get(ctx, path, &checks); err != nil { + return nil, err + } + return checks, nil +} + +func (c *Client) GetDeploymentCheck(ctx context.Context, projectID, checkID string) (*DeploymentCheck, error) { + var check DeploymentCheck + if err := c.get(ctx, fmt.Sprintf("/projects/%s/deployment_checks/%s", projectID, checkID), &check); err != nil { + return nil, err + } + return &check, nil +} + +func (c *Client) CreateDeploymentCheck(ctx context.Context, projectID string, req DeploymentCheckCreateRequest) (*DeploymentCheck, error) { + body := struct { + DeploymentCheck DeploymentCheckCreateRequest `json:"deployment_check"` + }{DeploymentCheck: req} + var check DeploymentCheck + if err := c.post(ctx, fmt.Sprintf("/projects/%s/deployment_checks", projectID), body, &check); err != nil { + return nil, err + } + return &check, nil +} + +func (c *Client) UpdateDeploymentCheck(ctx context.Context, projectID, checkID string, req DeploymentCheckCreateRequest) (*DeploymentCheck, error) { + body := struct { + DeploymentCheck DeploymentCheckCreateRequest `json:"deployment_check"` + }{DeploymentCheck: req} + var check DeploymentCheck + if err := c.put(ctx, fmt.Sprintf("/projects/%s/deployment_checks/%s", projectID, checkID), body, &check); err != nil { + return nil, err + } + return &check, nil +} + +func (c *Client) DeleteDeploymentCheck(ctx context.Context, projectID, checkID string) error { + return c.delete(ctx, fmt.Sprintf("/projects/%s/deployment_checks/%s", projectID, checkID)) +} diff --git a/pkg/sdk/deployment_checks_test.go b/pkg/sdk/deployment_checks_test.go new file mode 100644 index 0000000..43f654f --- /dev/null +++ b/pkg/sdk/deployment_checks_test.go @@ -0,0 +1,121 @@ +package sdk + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestListDeploymentChecks(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/projects/my-app/deployment_checks", r.URL.Path) + _ = json.NewEncoder(w).Encode([]DeploymentCheck{ + {Identifier: "chk1", Name: "Lint", Stage: "pre_build", CheckType: "ssh", Position: 1}, + {Identifier: "chk2", Name: "Smoke", Stage: "post_deploy", CheckType: "http", Position: 1}, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + checks, err := c.ListDeploymentChecks(context.Background(), "my-app", nil) + require.NoError(t, err) + assert.Len(t, checks, 2) + assert.Equal(t, "Lint", checks[0].Name) + assert.Equal(t, "http", checks[1].CheckType) +} + +func TestGetDeploymentCheck(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/projects/my-app/deployment_checks/chk1", r.URL.Path) + _ = json.NewEncoder(w).Encode(DeploymentCheck{ + Identifier: "chk1", Name: "Smoke test", Stage: "post_deploy", + CheckType: "http", HTTPMethod: "GET", HTTPURL: "https://example.com/health", + HTTPExpectedStatus: 200, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + check, err := c.GetDeploymentCheck(context.Background(), "my-app", "chk1") + require.NoError(t, err) + assert.Equal(t, "Smoke test", check.Name) + assert.Equal(t, "https://example.com/health", check.HTTPURL) + assert.Equal(t, 200, check.HTTPExpectedStatus) +} + +func TestCreateDeploymentCheck(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPost, r.Method) + + var body struct { + DeploymentCheck DeploymentCheckCreateRequest `json:"deployment_check"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + assert.Equal(t, "Lint", body.DeploymentCheck.Name) + assert.Equal(t, "pre_build", body.DeploymentCheck.Stage) + assert.Equal(t, "ssh", body.DeploymentCheck.CheckType) + assert.Equal(t, "bundle exec rubocop", body.DeploymentCheck.Command) + + w.WriteHeader(http.StatusCreated) + _ = json.NewEncoder(w).Encode(DeploymentCheck{ + Identifier: "chk-new", Name: "Lint", Stage: "pre_build", CheckType: "ssh", + Command: "bundle exec rubocop", + }) + })) + defer server.Close() + + c := newTestClient(t, server) + check, err := c.CreateDeploymentCheck(context.Background(), "my-app", DeploymentCheckCreateRequest{ + Name: "Lint", + Stage: "pre_build", + CheckType: "ssh", + Command: "bundle exec rubocop", + }) + require.NoError(t, err) + assert.Equal(t, "chk-new", check.Identifier) +} + +func TestUpdateDeploymentCheck(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodPut, r.Method) + assert.Equal(t, "/projects/my-app/deployment_checks/chk1", r.URL.Path) + + var body struct { + DeploymentCheck DeploymentCheckCreateRequest `json:"deployment_check"` + } + require.NoError(t, json.NewDecoder(r.Body).Decode(&body)) + require.NotNil(t, body.DeploymentCheck.Enabled) + assert.False(t, *body.DeploymentCheck.Enabled) + + _ = json.NewEncoder(w).Encode(DeploymentCheck{ + Identifier: "chk1", Name: "Lint", Enabled: false, + }) + })) + defer server.Close() + + c := newTestClient(t, server) + disabled := false + check, err := c.UpdateDeploymentCheck(context.Background(), "my-app", "chk1", DeploymentCheckCreateRequest{ + Enabled: &disabled, + }) + require.NoError(t, err) + assert.False(t, check.Enabled) +} + +func TestDeleteDeploymentCheck(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, http.MethodDelete, r.Method) + assert.Equal(t, "/projects/my-app/deployment_checks/chk1", r.URL.Path) + w.WriteHeader(http.StatusNoContent) + })) + defer server.Close() + + c := newTestClient(t, server) + err := c.DeleteDeploymentCheck(context.Background(), "my-app", "chk1") + require.NoError(t, err) +} From e7963038a536b256a6f496b2dc133d0be5dbc5c7 Mon Sep 17 00:00:00 2001 From: Facundo Farias Date: Thu, 21 May 2026 11:38:32 +0200 Subject: [PATCH 2/2] docs(deployment-checks): cover deployment-checks in README and skill guides Add the command to the README command list, the project-config decision trees in both SKILL guides, and a full Deployment Checks section in the configuration skill reference with examples for ssh, http, and vulnerability_scan checks. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 1 + docs/SKILL.md | 3 +- skills/deployhq/SKILL.md | 3 +- skills/deployhq/references/configuration.md | 90 +++++++++++++++++++++ 4 files changed, 95 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0951a0d..1dbadd4 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,7 @@ dhq config-files list | show | create | update | delete dhq build-commands list | create | update | delete dhq build-configs list | show | default | create | update | delete dhq ssh-commands list | show | create | update | delete +dhq deployment-checks list | show | create | update | delete dhq excluded-files list | show | create | update | delete dhq integrations list | show | create | update | delete dhq templates list | show | public | public-show | create | update | delete diff --git a/docs/SKILL.md b/docs/SKILL.md index edc135c..1c922e2 100644 --- a/docs/SKILL.md +++ b/docs/SKILL.md @@ -103,7 +103,8 @@ dhq api POST /projects//config_files --body '{"config_file":{...}}' 2. `dhq config-files list -p --json` → config files 3. `dhq build-commands list -p --json` → build pipeline 4. `dhq ssh-commands list -p --json` → SSH commands -5. `dhq servers list -p --json` → servers +5. `dhq deployment-checks list -p --json` → pre_build / post_deploy gates +6. `dhq servers list -p --json` → servers ## Invariants - Always use `--json` for machine-readable output diff --git a/skills/deployhq/SKILL.md b/skills/deployhq/SKILL.md index f74797c..94dbe2a 100644 --- a/skills/deployhq/SKILL.md +++ b/skills/deployhq/SKILL.md @@ -61,7 +61,7 @@ The only commands that **cannot** run non-interactively are: `dhq init`, `dhq he | **servers** | Manage deployment targets (SSH, FTP, S3, etc.) | [servers.md](references/servers.md) | | **deployments** | Create, monitor, rollback deployments | [deployments.md](references/deployments.md) | | **repos** | Repository configuration, branches, commits | [repos.md](references/repos.md) | -| **configuration** | Env vars, config files, build commands, exclusions, cache files, build languages, known hosts | [configuration.md](references/configuration.md) | +| **configuration** | Env vars, config files, build commands, exclusions, deployment checks, cache files, build languages, known hosts | [configuration.md](references/configuration.md) | | **global resources** | Global servers, env vars, config files, SSH keys, templates | [global-resources.md](references/global-resources.md) | | **operations** | Activity, status, test-access, doctor | [operations.md](references/operations.md) | | **auth & setup** | Authentication, CLI config, agent setup | [auth-setup.md](references/auth-setup.md) | @@ -94,6 +94,7 @@ The only commands that **cannot** run non-interactively are: `dhq init`, `dhq he 2. `dhq config-files create -p --path .env --body "..." --json` — add config file 3. `dhq excluded-files create -p --pattern "node_modules" --json` — add exclusion 4. `dhq build-commands create -p --name "Install" --command "npm install" --json` — add build step +5. `dhq deployment-checks create -p --name "Health" --stage post_deploy --check-type http --http-url https://app.example.com/health --http-expected-status 200 --json` — gate the deploy ### "Escape hatch (any API endpoint)" ``` diff --git a/skills/deployhq/references/configuration.md b/skills/deployhq/references/configuration.md index d2953ff..98c9ad6 100644 --- a/skills/deployhq/references/configuration.md +++ b/skills/deployhq/references/configuration.md @@ -1,3 +1,15 @@ +--- +tags: + - environment variables + - config files + - build commands + - build configuration + - deployment checks +tools: + - dhq + - snyk + - trivy +--- # Configuration Reference Project-level configuration: environment variables, config files, build commands, and exclusions. @@ -156,6 +168,84 @@ dhq ssh-commands list -p my-app --json dhq ssh-commands create -p my-app --name "Restart" --command "sudo systemctl restart app" --json ``` +## Deployment Checks + +Gate a deployment at a stage. A check has a `stage` (`pre_build` or `post_deploy`) and a `check_type`: +- `ssh` — runs a command over SSH on selected servers +- `http` — sends an HTTP request from the deployment worker +- `vulnerability_scan` — runs a security scanner on the build server (pre_build only) + +### `dhq deployment-checks list` +```bash +dhq deployment-checks list -p my-app --json +``` + +### `dhq deployment-checks show ` +```bash +dhq deployment-checks show chk_abc123 -p my-app --json +``` + +### `dhq deployment-checks create` + +| Flag | Required | Description | +|------|----------|-------------| +| `--name` | yes | Display name | +| `--stage` | yes | `pre_build` or `post_deploy` | +| `--check-type` | yes | `ssh`, `http`, or `vulnerability_scan` | +| `--enabled` | no | Whether the check runs (default `true`) | +| `--timeout` | no | Timeout in seconds | +| `--description` | no | Description | +| `--command` | ssh | Command to run on the target servers | +| `--servers` | ssh | Server identifiers to target (repeat or comma-separate) | +| `--http-method` | http | HTTP method (e.g. `GET`) | +| `--http-url` | http | URL to probe | +| `--http-expected-status` | http | Expected status code | +| `--http-body-match` | http | Substring expected in the response body | +| `--scanner` | vuln | `snyk`, `trivy`, or `custom` | +| `--scan-target-kind` | vuln | Target kind | +| `--scan-target` | vuln | Target path or identifier | +| `--severity-threshold` | vuln | Minimum severity that fails the check | +| `--fail-on-unfixed-only` | vuln | Only fail on findings with no available fix | +| `--sarif-output-path` | vuln | Where the scanner writes SARIF output | + +SSH check: +```bash +dhq deployment-checks create -p my-app \ + --name "Run migrations" --stage post_deploy --check-type ssh \ + --command "bundle exec rails db:migrate" --servers srv_prod1,srv_prod2 --json +``` + +HTTP check: +```bash +dhq deployment-checks create -p my-app \ + --name "Health check" --stage post_deploy --check-type http \ + --http-method GET --http-url https://app.example.com/health --http-expected-status 200 --json +``` + +Vulnerability scan (pre_build only): +```bash +dhq deployment-checks create -p my-app \ + --name "Snyk scan" --stage pre_build --check-type vulnerability_scan \ + --scanner snyk --severity-threshold high --fail-on-unfixed-only --json +``` + +### `dhq deployment-checks update ` + +Partial update — only flags you pass are sent. + +```bash +# Disable temporarily +dhq deployment-checks update chk_abc123 -p my-app --enabled=false --json + +# Tighten the severity gate +dhq deployment-checks update chk_abc123 -p my-app --severity-threshold critical --json +``` + +### `dhq deployment-checks delete ` +```bash +dhq deployment-checks delete chk_abc123 -p my-app +``` + ## Build Cache Files Manage files/directories cached between builds to speed up deployments.