Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 24 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,27 @@ for complete information on tag formatting.
}, "Client", "<another tag>"]
}
```

### GitHub Status Check Polling

By default, when GitHub integration is enabled, the deploy tool will poll GitHub status checks before proceeding with deployments. This ensures that all CI checks pass before deployment begins.

The polling behavior can be configured in your `.deploy` file:

```JSON
{
"github": {
"polling": {
"timeout": 1800000,
"interval": 30000
}
}
}
```

**Configuration Options:**

- `timeout`: Maximum time to wait for status checks in milliseconds (1 minute to 1 hour, default: 30 minutes)
- `interval`: Time between status check polls in milliseconds (5 seconds to 5 minutes, default: 30 seconds)

If any status checks fail, the deployment will be aborted with an error message indicating which checks failed.
11 changes: 11 additions & 0 deletions cli.js
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,17 @@ async function main() {
process.exit(1);
}

// Poll GitHub status checks before proceeding with deployment
if (context.github) {
try {
await gh.pollStatusChecks(context.githubPolling);
} catch (err) {
console.error(`Status Check Polling Failed: ${err.message}`);
if (argv.debug) throw err;
process.exit(1);
}
}

if (context.github) await gh.deployment(argv._[3]);

if (context.tags && ['create', 'update'].includes(command)) {
Expand Down
28 changes: 28 additions & 0 deletions data/schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,34 @@
}
}]
}
},
"github": {
"type": "object",
"additionalProperties": false,
"description": "GitHub integration settings",
"properties": {
"polling": {
"type": "object",
"additionalProperties": false,
"description": "Status check polling configuration",
"properties": {
"timeout": {
"type": "number",
"minimum": 60000,
"maximum": 3600000,
"default": 1800000,
"description": "Timeout for status check polling in milliseconds (1 minute to 1 hour, default: 30 minutes)"
},
"interval": {
"type": "number",
"minimum": 5000,
"maximum": 300000,
"default": 30000,
"description": "Interval between status check polls in milliseconds (5 seconds to 5 minutes, default: 30 seconds)"
}
}
}
}
}
}
}
15 changes: 15 additions & 0 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,21 @@ export default class Credentials {
creds.tags = creds.tags.concat(creds.dotdeploy.tags || []);
}

// Load GitHub polling configuration
creds.githubPolling = {
timeout: 30 * 60 * 1000, // 30 minutes default
interval: 30 * 1000 // 30 seconds default
};

if (creds.dotdeploy.github?.polling) {
if (creds.dotdeploy.github.polling.timeout) {
creds.githubPolling.timeout = creds.dotdeploy.github.polling.timeout;
}
if (creds.dotdeploy.github.polling.interval) {
creds.githubPolling.interval = creds.dotdeploy.github.polling.interval;
}
}

creds.aws = {};

try {
Expand Down
67 changes: 63 additions & 4 deletions lib/gh.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import fs from 'fs';
import ora from 'ora';
import Git from './git.js';

/**
Expand Down Expand Up @@ -64,19 +63,79 @@ export default class GH {
async status() {
const res = await fetch(this.url + `/repos/${this.context.owner}/${this.repo}/commits/${this.context.sha}/status`, {
method: 'GET',
headers: this.headers,
headers: this.headers
});

const body = await res.json();

if (!res.ok) {
console.error(body);
throw new Error('Could not list status checks');
} else {
if (body.state === 'pending') {
}

return body;
}

/**
* Poll GitHub status checks until they pass or fail
*
* @param {Object} options - Polling options
* @param {number} options.timeout - Timeout in milliseconds (default: 30 minutes)
* @param {number} options.interval - Poll interval in milliseconds (default: 30 seconds)
* @returns {Promise<boolean>} - True if checks pass, throws error if they fail
*/
async pollStatusChecks(options = {}) {
const timeout = options.timeout || 30 * 60 * 1000; // 30 minutes default
const interval = options.interval || 30 * 1000; // 30 seconds default
const startTime = Date.now();

console.log(`Polling GitHub status checks for commit ${this.context.sha}...`);

while (Date.now() - startTime < timeout) {
try {
const status = await this.status();

console.log(`Status: ${status.state} (${status.statuses?.length || 0} checks)`);

if (status.state === 'success') {
console.log('✅ All status checks passed!');
return true;
} else if (status.state === 'failure') {
const failedChecks = status.statuses?.filter((s) => s.state === 'failure') || [];
const errorMessage = failedChecks.length > 0
? `Failed checks: ${failedChecks.map((c) => c.context).join(', ')}`
: 'Some status checks failed';
throw new Error(`❌ Status checks failed. ${errorMessage}`);
} else if (status.state === 'error') {
const errorChecks = status.statuses?.filter((s) => s.state === 'error') || [];
const errorMessage = errorChecks.length > 0
? `Error in checks: ${errorChecks.map((c) => c.context).join(', ')}`
: 'Some status checks encountered errors';
throw new Error(`❌ Status checks encountered errors. ${errorMessage}`);
} else if (status.state === 'pending') {
const pendingChecks = status.statuses?.filter((s) => s.state === 'pending') || [];
if (pendingChecks.length > 0) {
console.log(`⏳ Waiting for: ${pendingChecks.map((c) => c.context).join(', ')}`);
}

// Wait before next poll
await new Promise((resolve) => setTimeout(resolve, interval));
} else {
// Handle unexpected states
console.log(`⚠️ Unexpected status state: ${status.state}`);
await new Promise((resolve) => setTimeout(resolve, interval));
}
} catch (error) {
if (error.message.includes('Status checks failed') || error.message.includes('encountered errors')) {
throw error; // Re-throw status check failures
}

console.error(`Error polling status checks: ${error.message}`);
await new Promise((resolve) => setTimeout(resolve, interval));
}
}

throw new Error(`❌ Timeout waiting for status checks to complete after ${timeout / 1000 / 60} minutes`);
}

async deployment_list(stack) {
Expand Down