Skip to content
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -207,3 +207,4 @@ for complete information on tag formatting.
}, "Client", "<another tag>"]
}
```

6 changes: 6 additions & 0 deletions lib/context.js
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,12 @@ 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
};

creds.aws = {};

try {
Expand Down
67 changes: 61 additions & 6 deletions lib/gh.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,13 @@ export default class GH {
success = 'failed';
}

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

if (this.context.deployment) {
return await this.deployment_update(stack, success);
Expand All @@ -62,20 +68,69 @@ export default class GH {
}

async status() {
const res = await fetch(this.url + `/repos/${this.context.owner}/${this.repo}/commits/${this.context.sha}/status`, {
const res = await fetch(this.url + `/repos/${this.context.owner}/${this.repo}/commits/${this.context.sha}/check-runs`, {
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') {
throw new Error('Could not list checks');
}

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();

const progress = ora(`GitHub Status Checks: ${this.context.sha}`).start();

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

progress.text = `GitHub Status Checks: (${status.check_runs?.length || 0} checks)`;

const completed = status.check_runs?.filter((s) => s.status === 'completed') || [];

if (completed.length === status.check_runs?.length) {
progress.success();
return
};

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
}

progress.text = `GitHub Status Checks: Error - ${error.message}`;
await new Promise((resolve) => setTimeout(resolve, interval));
}
}

progress.fail(`GitHub Status Checks: Timeout after ${timeout / 1000 / 60} minutes`);
throw new Error(`❌ Timeout waiting for status checks to complete after ${timeout / 1000 / 60} minutes`);
} catch (error) {
// Ensure we clean up the spinner if it's still running
if (progress.isSpinning) {
progress.fail('GitHub Status Checks: Failed');
}
throw error;
}
}

Expand Down