Skip to content

Commit 06cb54a

Browse files
authored
Merge pull request #1 from versioner-io/pre-flight
Pre flight
2 parents 36bf397 + 2012571 commit 06cb54a

4 files changed

Lines changed: 396 additions & 34 deletions

File tree

README.md

Lines changed: 198 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,203 @@ Both build and deployment events support these statuses:
183183

184184
Aliases like `success`, `in_progress`, `cancelled`, etc. are automatically normalized.
185185

186+
## Preflight Checks
187+
188+
When tracking a deployment with `--status=started`, the API automatically runs **preflight checks** to validate the deployment before it proceeds. These checks help enforce deployment policies and prevent common issues.
189+
190+
### What Gets Checked
191+
192+
Preflight checks validate:
193+
194+
1. **Concurrent Deployments** - Prevents multiple simultaneous deployments to the same environment
195+
2. **No-Deploy Windows** - Blocks deployments during scheduled blackout periods (e.g., Friday afternoons)
196+
3. **Flow Requirements** - Ensures versions are deployed to prerequisite environments first (e.g., staging before production)
197+
4. **Soak Time** - Requires versions to run in an environment for a minimum duration before promoting
198+
5. **Quality Approvals** - Requires QA/security sign-off from prerequisite environments
199+
6. **Release Approvals** - Requires manager/lead approval before deploying to sensitive environments
200+
201+
### Exit Codes
202+
203+
The CLI uses specific exit codes to indicate different failure types:
204+
205+
- **0** - Success (deployment allowed)
206+
- **1** - General error (network issues, invalid arguments)
207+
- **4** - API error (authentication, validation)
208+
- **5** - Preflight check failure (deployment blocked)
209+
210+
### Error Types and Responses
211+
212+
#### 409 - Deployment Conflict
213+
214+
Another deployment is already in progress:
215+
216+
```
217+
⚠️ Deployment Conflict
218+
219+
Another deployment to production is already in progress
220+
Another deployment is in progress. Please wait and retry.
221+
```
222+
223+
**Action:** Wait for the current deployment to complete, then retry.
224+
225+
#### 423 - Schedule Block
226+
227+
Deployment blocked by a no-deploy window:
228+
229+
```
230+
🔒 Deployment Blocked by Schedule
231+
232+
Rule: Production Freeze - Friday Afternoons
233+
Deployment blocked by no-deploy window
234+
235+
Retry after: 2025-11-21T18:00:00-08:00
236+
237+
To skip checks (emergency only), add:
238+
--skip-preflight-checks
239+
```
240+
241+
**Action:** Wait until the blackout window ends, or use `--skip-preflight-checks` for emergencies.
242+
243+
#### 428 - Precondition Failed
244+
245+
Missing required prerequisites:
246+
247+
**Flow Violation:**
248+
```
249+
❌ Deployment Precondition Failed
250+
251+
Error: FLOW_VIOLATION
252+
Rule: Staging Required Before Production
253+
Version must be deployed to staging first
254+
255+
Deploy to required environments first, then retry.
256+
```
257+
258+
**Insufficient Soak Time:**
259+
```
260+
❌ Deployment Precondition Failed
261+
262+
Error: INSUFFICIENT_SOAK_TIME
263+
Rule: 24hr Staging Soak
264+
Version must soak in staging for at least 24 hours
265+
266+
Retry after: 2025-11-22T10:00:00Z
267+
268+
Wait for soak time to complete, then retry.
269+
```
270+
271+
**Approval Required:**
272+
```
273+
❌ Deployment Precondition Failed
274+
275+
Error: APPROVAL_REQUIRED
276+
Rule: Prod Needs 2 Approvals
277+
production deployment requires 2 release approval(s)
278+
279+
Approval required before deployment can proceed.
280+
Obtain approval via Versioner UI, then retry.
281+
```
282+
283+
### Emergency Override
284+
285+
For production incidents or hotfixes, you can skip preflight checks:
286+
287+
```bash
288+
versioner track deployment \
289+
--product=api-service \
290+
--environment=production \
291+
--version=1.2.3-hotfix \
292+
--status=started \
293+
--skip-preflight-checks
294+
```
295+
296+
**⚠️ Warning:** Only use `--skip-preflight-checks` for:
297+
- Production incidents requiring immediate fixes
298+
- Approved emergency changes
299+
- When deployment rules are temporarily misconfigured
300+
301+
Always document why checks were skipped in your deployment logs.
302+
303+
### Full Deployment Workflow
304+
305+
```bash
306+
# 1. Start deployment (triggers preflight checks)
307+
versioner track deployment \
308+
--product=api-service \
309+
--environment=production \
310+
--version=1.2.3 \
311+
--status=started
312+
313+
# 2. If checks pass (exit code 0), proceed with actual deployment
314+
if [ $? -eq 0 ]; then
315+
# Your deployment commands here
316+
kubectl apply -f deployment.yaml
317+
318+
# 3. Report completion
319+
versioner track deployment \
320+
--product=api-service \
321+
--environment=production \
322+
--version=1.2.3 \
323+
--status=completed
324+
fi
325+
```
326+
327+
### CI/CD Integration
328+
329+
**GitHub Actions:**
330+
```yaml
331+
- name: Start Deployment
332+
id: preflight
333+
run: |
334+
versioner track deployment \
335+
--product=api-service \
336+
--environment=production \
337+
--version=${{ github.sha }} \
338+
--status=started
339+
env:
340+
VERSIONER_API_KEY: ${{ secrets.VERSIONER_API_KEY }}
341+
continue-on-error: true
342+
343+
- name: Deploy Application
344+
if: steps.preflight.outcome == 'success'
345+
run: |
346+
# Your deployment commands
347+
kubectl apply -f k8s/
348+
349+
- name: Report Completion
350+
if: steps.preflight.outcome == 'success'
351+
run: |
352+
versioner track deployment \
353+
--product=api-service \
354+
--environment=production \
355+
--version=${{ github.sha }} \
356+
--status=completed
357+
env:
358+
VERSIONER_API_KEY: ${{ secrets.VERSIONER_API_KEY }}
359+
```
360+
361+
**GitLab CI:**
362+
```yaml
363+
deploy:production:
364+
script:
365+
# Preflight check
366+
- |
367+
versioner track deployment \
368+
--product=api \
369+
--environment=production \
370+
--version=$CI_COMMIT_SHA \
371+
--status=started
372+
# Deploy if checks pass
373+
- kubectl apply -f k8s/
374+
# Report completion
375+
- |
376+
versioner track deployment \
377+
--product=api \
378+
--environment=production \
379+
--version=$CI_COMMIT_SHA \
380+
--status=completed
381+
```
382+
186383
## CI/CD Auto-Detection
187384
188385
The CLI automatically detects your CI/CD environment and extracts relevant metadata. Supported systems:
@@ -297,9 +494,7 @@ This is a beta release and we'd love your feedback!
297494

298495
For comprehensive documentation:
299496

300-
- **[API Reference](https://api.versioner.io/docs)** - Interactive OpenAPI documentation
301-
- **[Web Dashboard](https://app.versioner.io)** - View your deployment history
302-
- **[CLI Integration Guide](https://github.com/versioner-io/versioner-docs/blob/main/features/cli-integration.md)** - Complete feature documentation and roadmap
497+
- See [Versioner docs](https://docs.versioner.io)
303498

304499
### Repository-Specific Docs
305500

internal/api/client.go

Lines changed: 46 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -135,13 +135,23 @@ func handleResponse(resp *http.Response, result interface{}) error {
135135
}
136136

137137
// Error response
138-
var apiError APIError
139-
if err := json.Unmarshal(body, &apiError); err != nil {
138+
var errorResponse struct {
139+
Detail interface{} `json:"detail"`
140+
}
141+
if err := json.Unmarshal(body, &errorResponse); err != nil {
140142
// Fallback if error response doesn't match expected format
141-
return fmt.Errorf("API error (HTTP %d): %s", resp.StatusCode, string(body))
143+
return &APIError{
144+
StatusCode: resp.StatusCode,
145+
Detail: string(body),
146+
}
147+
}
148+
149+
apiError := &APIError{
150+
StatusCode: resp.StatusCode,
151+
Detail: errorResponse.Detail,
142152
}
143153

144-
return &apiError
154+
return apiError
145155
}
146156

147157
// APIError represents an error response from the API
@@ -164,3 +174,35 @@ func (e *APIError) Error() string {
164174
return fmt.Sprintf("API error: %v", detail)
165175
}
166176
}
177+
178+
// IsPreflightError checks if this is a preflight check failure (409, 423, 428)
179+
func (e *APIError) IsPreflightError() bool {
180+
return e.StatusCode == 409 || e.StatusCode == 423 || e.StatusCode == 428
181+
}
182+
183+
// GetPreflightDetails extracts structured preflight error details
184+
func (e *APIError) GetPreflightDetails() (errorType, message, code, retryAfter string, details map[string]interface{}, ok bool) {
185+
detailMap, ok := e.Detail.(map[string]interface{})
186+
if !ok {
187+
return
188+
}
189+
190+
if errType, exists := detailMap["error"].(string); exists {
191+
errorType = errType
192+
}
193+
if msg, exists := detailMap["message"].(string); exists {
194+
message = msg
195+
}
196+
if c, exists := detailMap["code"].(string); exists {
197+
code = c
198+
}
199+
if retry, exists := detailMap["retry_after"].(string); exists {
200+
retryAfter = retry
201+
}
202+
if det, exists := detailMap["details"].(map[string]interface{}); exists {
203+
details = det
204+
}
205+
206+
ok = true
207+
return
208+
}

internal/api/deployment.go

Lines changed: 26 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,22 @@ import "time"
44

55
// DeploymentEventCreate represents the request payload for creating a deployment event
66
type DeploymentEventCreate struct {
7-
ProductName string `json:"product_name"`
8-
Version string `json:"version"`
9-
EnvironmentName string `json:"environment_name"`
10-
Status string `json:"status"`
11-
SourceSystem string `json:"source_system,omitempty"`
12-
BuildNumber string `json:"build_number,omitempty"`
13-
SCMSha string `json:"scm_sha,omitempty"`
14-
SCMRepository string `json:"scm_repository,omitempty"`
15-
BuildURL string `json:"build_url,omitempty"`
16-
InvokeID string `json:"invoke_id,omitempty"`
17-
DeployedBy string `json:"deployed_by,omitempty"`
18-
DeployedByEmail string `json:"deployed_by_email,omitempty"`
19-
DeployedByName string `json:"deployed_by_name,omitempty"`
20-
CompletedAt *time.Time `json:"completed_at,omitempty"`
21-
ExtraMetadata map[string]interface{} `json:"extra_metadata,omitempty"`
7+
ProductName string `json:"product_name"`
8+
Version string `json:"version"`
9+
EnvironmentName string `json:"environment_name"`
10+
Status string `json:"status"`
11+
SourceSystem string `json:"source_system,omitempty"`
12+
BuildNumber string `json:"build_number,omitempty"`
13+
SCMSha string `json:"scm_sha,omitempty"`
14+
SCMRepository string `json:"scm_repository,omitempty"`
15+
BuildURL string `json:"build_url,omitempty"`
16+
InvokeID string `json:"invoke_id,omitempty"`
17+
DeployedBy string `json:"deployed_by,omitempty"`
18+
DeployedByEmail string `json:"deployed_by_email,omitempty"`
19+
DeployedByName string `json:"deployed_by_name,omitempty"`
20+
CompletedAt *time.Time `json:"completed_at,omitempty"`
21+
SkipPreflightChecks bool `json:"skip_preflight_checks,omitempty"`
22+
ExtraMetadata map[string]interface{} `json:"extra_metadata,omitempty"`
2223
}
2324

2425
// DeploymentResponse represents the response from creating a deployment event
@@ -31,6 +32,16 @@ type DeploymentResponse struct {
3132
DeployedAt *time.Time `json:"deployed_at,omitempty"`
3233
}
3334

35+
// PreflightError represents a preflight check failure with detailed information
36+
type PreflightError struct {
37+
StatusCode int
38+
Error string `json:"error"`
39+
Message string `json:"message"`
40+
Code string `json:"code"`
41+
Details map[string]interface{} `json:"details"`
42+
RetryAfter string `json:"retry_after,omitempty"`
43+
}
44+
3445
// CreateDeploymentEvent sends a deployment event to the API
3546
func (c *Client) CreateDeploymentEvent(event *DeploymentEventCreate) (*DeploymentResponse, error) {
3647
resp, err := c.doRequest("POST", "/deployment-events/", event)

0 commit comments

Comments
 (0)