Skip to content

Commit b451d8e

Browse files
authored
Merge pull request #1 from versioner-io/pre-flight
feat: Add preflight checks integration with skip_preflight_checks input
2 parents 61f2f24 + 9ee87a5 commit b451d8e

13 files changed

Lines changed: 279 additions & 10 deletions

README.md

Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ Check the **Actions** tab to see your deployment tracked!
8686
| `status` || `success` | Event status (`success`, `failure`, `in_progress`) |
8787
| `metadata` || `{}` | Additional JSON metadata to attach to the event |
8888
| `fail_on_rejection` || `true` | Fail the workflow if Versioner rejects the deployment (e.g., conflicts, no-deploy windows) |
89+
| `skip_preflight_checks` || `false` | Skip preflight checks (use for emergency deployments only) |
8990

9091
\* Required unless provided via `VERSIONER_API_KEY` environment variable
9192

@@ -99,6 +100,93 @@ Check the **Actions** tab to see your deployment tracked!
99100
| `version_id` | UUID of the version record (all events) |
100101
| `product_id` | UUID of the product (all events) |
101102

103+
## 🛡️ Preflight Checks
104+
105+
When starting a deployment (`status: started`), Versioner automatically runs preflight checks to validate:
106+
- **No concurrent deployments** - Prevents multiple simultaneous deployments to the same environment
107+
- **No active no-deploy windows** - Respects scheduled freeze periods (e.g., Friday afternoons, holidays)
108+
- **Required approvals obtained** - Ensures proper authorization before deployment
109+
- **Flow/soak time requirements met** - Validates promotion path and minimum soak time in lower environments
110+
111+
If checks fail, the action will fail and the deployment will **NOT** be created.
112+
113+
### Default Behavior
114+
115+
Preflight checks run automatically by default:
116+
117+
```yaml
118+
- name: Deploy to production
119+
uses: versioner-io/versioner-github-action@v1
120+
with:
121+
api_key: ${{ secrets.VERSIONER_API_KEY }}
122+
product_name: my-service
123+
version: ${{ github.sha }}
124+
environment: production
125+
status: started # Checks run automatically
126+
```
127+
128+
### Skip Checks (Emergency Only)
129+
130+
For emergency hotfixes, you can skip preflight checks:
131+
132+
```yaml
133+
- name: Emergency hotfix deployment
134+
uses: versioner-io/versioner-github-action@v1
135+
with:
136+
api_key: ${{ secrets.VERSIONER_API_KEY }}
137+
product_name: my-service
138+
version: ${{ github.sha }}
139+
environment: production
140+
status: started
141+
skip_preflight_checks: true # ⚠️ Use sparingly!
142+
```
143+
144+
**⚠️ Warning:** Skipping checks bypasses all deployment policies. Use only for genuine emergencies.
145+
146+
### Error Messages
147+
148+
When preflight checks fail, you'll see detailed error messages:
149+
150+
**Schedule Block (423):**
151+
```
152+
🔒 Deployment Blocked by Schedule
153+
154+
Rule: Production Freeze - Friday Afternoons
155+
Deployment blocked by no-deploy window
156+
157+
Retry after: 2025-11-21T18:00:00-08:00
158+
159+
To skip checks (emergency only), add to your workflow:
160+
skip-preflight-checks: true
161+
```
162+
163+
**Flow Violation (428):**
164+
```
165+
❌ Deployment Precondition Failed
166+
167+
Error: FLOW_VIOLATION
168+
Rule: Staging Required Before Production
169+
Version must be deployed to staging first
170+
171+
Deploy to required environments first, then retry.
172+
```
173+
174+
**Insufficient Soak Time (428):**
175+
```
176+
❌ Deployment Precondition Failed
177+
178+
Error: INSUFFICIENT_SOAK_TIME
179+
Rule: 24hr Staging Soak
180+
Version must soak in staging for at least 24 hours
181+
182+
Retry after: 2025-11-22T10:00:00Z
183+
184+
Wait for soak time to complete, then retry.
185+
186+
To skip checks (emergency only), add to your workflow:
187+
skip-preflight-checks: true
188+
```
189+
102190
## 🔧 Usage Examples
103191

104192
### Using Environment Variables (Recommended for Multiple Events)

action.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,10 @@ inputs:
4242
description: 'Fail the workflow if Versioner rejects the deployment (e.g., conflicts, no-deploy windows)'
4343
required: false
4444
default: 'true'
45+
skip_preflight_checks:
46+
description: 'Skip preflight checks (use for emergency deployments only)'
47+
required: false
48+
default: 'false'
4549

4650
outputs:
4751
deployment_id:

dist/api-client.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/index.js

Lines changed: 63 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -34598,9 +34598,64 @@ async function sendDeploymentEvent(apiUrl, apiKey, payload, failOnRejection = fa
3459834598
const data = axiosError.response?.data;
3459934599
// Handle rejection status codes (409, 423, 428)
3460034600
if (status === 409 || status === 423 || status === 428) {
34601-
const errorData = data;
34602-
const message = errorData?.message || errorData?.error || 'Deployment rejected by Versioner';
34603-
const rejectionError = `Deployment rejected: ${message}`;
34601+
const errorResponse = data;
34602+
const detail = errorResponse?.detail;
34603+
const errorCode = detail?.code || 'UNKNOWN';
34604+
const message = detail?.message || 'Deployment rejected by Versioner';
34605+
const ruleName = detail?.details?.rule_name || 'Unknown Rule';
34606+
let rejectionError = '';
34607+
// Format error based on status code
34608+
if (status === 409) {
34609+
// DEPLOYMENT_IN_PROGRESS
34610+
rejectionError = `⚠️ Deployment Conflict\n\n`;
34611+
rejectionError += `${message}\n`;
34612+
rejectionError += `Another deployment is in progress. Please wait and retry.`;
34613+
}
34614+
else if (status === 423) {
34615+
// NO_DEPLOY_WINDOW
34616+
rejectionError = `🔒 Deployment Blocked by Schedule\n\n`;
34617+
rejectionError += `Rule: ${ruleName}\n`;
34618+
rejectionError += `${message}\n`;
34619+
if (detail?.retry_after) {
34620+
rejectionError += `\nRetry after: ${detail.retry_after}`;
34621+
}
34622+
rejectionError += `\n\nTo skip checks (emergency only), add to your workflow:\n`;
34623+
rejectionError += ` skip-preflight-checks: true`;
34624+
}
34625+
else if (status === 428) {
34626+
// Precondition failures (FLOW_VIOLATION, INSUFFICIENT_SOAK_TIME, etc.)
34627+
rejectionError = `❌ Deployment Precondition Failed\n\n`;
34628+
rejectionError += `Error: ${errorCode}\n`;
34629+
rejectionError += `Rule: ${ruleName}\n`;
34630+
rejectionError += `${message}\n`;
34631+
if (detail?.retry_after) {
34632+
rejectionError += `\nRetry after: ${detail.retry_after}`;
34633+
}
34634+
// Add specific guidance based on error code
34635+
if (errorCode === 'FLOW_VIOLATION') {
34636+
rejectionError += `\n\nDeploy to required environments first, then retry.`;
34637+
}
34638+
else if (errorCode === 'INSUFFICIENT_SOAK_TIME') {
34639+
rejectionError += `\n\nWait for soak time to complete, then retry.`;
34640+
rejectionError += `\n\nTo skip checks (emergency only), add to your workflow:\n`;
34641+
rejectionError += ` skip-preflight-checks: true`;
34642+
}
34643+
else if (errorCode === 'QUALITY_APPROVAL_REQUIRED' ||
34644+
errorCode === 'APPROVAL_REQUIRED') {
34645+
rejectionError += `\n\nApproval required before deployment can proceed.`;
34646+
rejectionError += `\nObtain approval via Versioner UI, then retry.`;
34647+
}
34648+
else {
34649+
// Generic handler for unknown/future error codes
34650+
rejectionError += `\n\nResolve the issue described above, then retry.`;
34651+
rejectionError += `\n\nTo skip checks (emergency only), add to your workflow:\n`;
34652+
rejectionError += ` skip-preflight-checks: true`;
34653+
}
34654+
// Always include full details for debugging (all error codes)
34655+
if (detail?.details) {
34656+
rejectionError += `\n\nDetails: ${JSON.stringify(detail.details, null, 2)}`;
34657+
}
34658+
}
3460434659
if (failOnRejection) {
3460534660
throw new Error(rejectionError);
3460634661
}
@@ -35025,6 +35080,7 @@ async function run() {
3502535080
version: inputs.version,
3502635081
environment_name: inputs.environment,
3502735082
status: inputs.status,
35083+
skip_preflight_checks: inputs.skipPreflightChecks,
3502835084
scm_repository: githubMetadata.scm_repository,
3502935085
scm_sha: githubMetadata.scm_sha,
3503035086
source_system: githubMetadata.source_system,
@@ -35124,6 +35180,7 @@ function getInputs() {
3512435180
const status = core.getInput('status', { required: false }) || 'success';
3512535181
const metadataInput = core.getInput('metadata', { required: false }) || '{}';
3512635182
const failOnRejectionInput = core.getInput('fail_on_rejection', { required: false }) || 'true';
35183+
const skipPreflightChecksInput = core.getInput('skip_preflight_checks', { required: false }) || 'false';
3512735184
// Validate API key is provided
3512835185
if (!apiKey) {
3512935186
throw new Error(`api_key is required (provide via input or VERSIONER_API_KEY environment variable)`);
@@ -35159,6 +35216,8 @@ function getInputs() {
3515935216
}
3516035217
// Parse fail_on_rejection boolean
3516135218
const failOnRejection = failOnRejectionInput.toLowerCase() === 'true';
35219+
// Parse skip_preflight_checks boolean
35220+
const skipPreflightChecks = skipPreflightChecksInput.toLowerCase() === 'true';
3516235221
return {
3516335222
apiUrl,
3516435223
apiKey,
@@ -35169,6 +35228,7 @@ function getInputs() {
3516935228
status,
3517035229
metadata,
3517135230
failOnRejection,
35231+
skipPreflightChecks,
3517235232
};
3517335233
}
3517435234

dist/index.js.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/inputs.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dist/types.d.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export interface DeploymentEventPayload {
66
version: string;
77
environment_name?: string;
88
status: string;
9+
skip_preflight_checks?: boolean;
910
scm_repository?: string;
1011
scm_sha?: string;
1112
source_system?: string;
@@ -68,6 +69,7 @@ export interface ActionInputs {
6869
status: string;
6970
metadata: Record<string, unknown>;
7071
failOnRejection: boolean;
72+
skipPreflightChecks: boolean;
7173
}
7274
export interface GitHubMetadata {
7375
scm_repository: string;

dist/types.d.ts.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/__tests__/inputs.test.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ describe('getInputs', () => {
4545
status: 'success',
4646
metadata: {},
4747
failOnRejection: true,
48+
skipPreflightChecks: false,
4849
})
4950
})
5051

@@ -259,4 +260,40 @@ describe('getInputs', () => {
259260

260261
expect(inputs.failOnRejection).toBe(false)
261262
})
263+
264+
it('should parse skip_preflight_checks as false by default', () => {
265+
mockGetInput.mockImplementation((name: string) => {
266+
const inputs: Record<string, string> = {
267+
api_url: 'https://api.versioner.io',
268+
api_key: 'sk_test_key',
269+
product_name: 'test-product',
270+
version: '1.0.0',
271+
environment: 'production',
272+
skip_preflight_checks: '',
273+
}
274+
return inputs[name] || ''
275+
})
276+
277+
const inputs = getInputs()
278+
279+
expect(inputs.skipPreflightChecks).toBe(false)
280+
})
281+
282+
it('should parse skip_preflight_checks as true when explicitly set', () => {
283+
mockGetInput.mockImplementation((name: string) => {
284+
const inputs: Record<string, string> = {
285+
api_url: 'https://api.versioner.io',
286+
api_key: 'sk_test_key',
287+
product_name: 'test-product',
288+
version: '1.0.0',
289+
environment: 'production',
290+
skip_preflight_checks: 'true',
291+
}
292+
return inputs[name] || ''
293+
})
294+
295+
const inputs = getInputs()
296+
297+
expect(inputs.skipPreflightChecks).toBe(true)
298+
})
262299
})

0 commit comments

Comments
 (0)