Hooks System
Run custom logic at different stages of the OTA publishing process.
Overview
Hooks allow you to:
- ✅ Run tests before publishing
- ✅ Send notifications after publishing
- ✅ Handle errors gracefully
- ✅ Integrate with CI/CD
- ✅ Update documentation
- ✅ Track analytics
Configuration
Add hooks to ota-updates.config.js:
javascript
export default {
hooks: {
beforePublish: async (context) => {
// Runs before publishing
},
afterPublish: async (version) => {
// Runs after successful publish
},
onError: async (error) => {
// Runs on publish error
},
},
};Hook Types
beforePublish
Runs before the update is published.
Parameters:
typescript
{
currentVersion: OTAVersionData | null;
channel: string;
changelog: string[];
}Example:
javascript
beforePublish: async ({ currentVersion, channel, changelog }) => {
console.log(`Publishing to ${channel}...`);
console.log(`Current version: ${currentVersion?.version}`);
console.log(`Changes: ${changelog.length} items`);
// Run tests
await execa('npm', ['test']);
// Run linting
await execa('npm', ['run', 'lint']);
// Validate changelog
if (changelog.length === 0) {
throw new Error('Changelog cannot be empty');
}
}afterPublish
Runs after successful publish.
Parameters:
typescript
{
version: string;
buildNumber: number;
releaseDate: string;
channel: string;
changelog: string[];
}Example:
javascript
afterPublish: async (version) => {
console.log(`✓ Published ${version.version}`);
// Send Slack notification
await fetch('https://hooks.slack.com/...', {
method: 'POST',
body: JSON.stringify({
text: `🚀 New OTA update published!\nVersion: ${version.version}\nChannel: ${version.channel}`,
}),
});
// Update CHANGELOG.md
const changelog = `## ${version.version} - ${new Date().toISOString().split('T')[0]}\n\n${version.changelog.map(c => `- ${c}`).join('\n')}\n\n`;
await fs.appendFile('CHANGELOG.md', changelog);
}onError
Runs when publish fails.
Parameters:
typescript
ErrorExample:
javascript
onError: async (error) => {
console.error('Publish failed:', error.message);
// Send error alert
await fetch('https://hooks.slack.com/...', {
method: 'POST',
body: JSON.stringify({
text: `❌ OTA publish failed!\nError: ${error.message}`,
}),
});
// Log to error tracking
await Sentry.captureException(error);
}Common Use Cases
Run Tests
javascript
beforePublish: async () => {
const { execa } = await import('execa');
console.log('Running tests...');
await execa('npm', ['test']);
console.log('Running type check...');
await execa('npm', ['run', 'typecheck']);
}Send Notifications
javascript
afterPublish: async (version) => {
// Slack
await fetch(process.env.SLACK_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
text: `🚀 OTA Update Published`,
blocks: [
{
type: 'section',
text: {
type: 'mrkdwn',
text: `*Version:* ${version.version}\n*Channel:* ${version.channel}\n*Changes:*\n${version.changelog.map(c => `• ${c}`).join('\n')}`,
},
},
],
}),
});
// Discord
await fetch(process.env.DISCORD_WEBHOOK_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: `🚀 New OTA update: ${version.version}`,
embeds: [{
title: 'Changelog',
description: version.changelog.join('\n'),
}],
}),
});
}Update Documentation
javascript
afterPublish: async (version) => {
const fs = await import('fs/promises');
// Update CHANGELOG.md
const date = new Date().toISOString().split('T')[0];
const entry = `## [${version.version}] - ${date}\n\n${version.changelog.map(c => `- ${c}`).join('\n')}\n\n`;
const changelog = await fs.readFile('CHANGELOG.md', 'utf-8');
await fs.writeFile('CHANGELOG.md', entry + changelog);
// Commit and push
const { execa } = await import('execa');
await execa('git', ['add', 'CHANGELOG.md', 'ota-version.json']);
await execa('git', ['commit', '-m', `chore: release ${version.version}`]);
await execa('git', ['push']);
}Track Analytics
javascript
afterPublish: async (version) => {
// Track in analytics
await fetch('https://api.analytics.com/events', {
method: 'POST',
headers: { 'Authorization': `Bearer ${process.env.ANALYTICS_TOKEN}` },
body: JSON.stringify({
event: 'ota_publish',
properties: {
version: version.version,
channel: version.channel,
buildNumber: version.buildNumber,
changelogItems: version.changelog.length,
},
}),
});
}Validate Before Publish
javascript
beforePublish: async ({ channel, changelog }) => {
// Production requires approval
if (channel === 'production') {
const prompts = await import('prompts');
const { confirmed } = await prompts({
type: 'confirm',
name: 'confirmed',
message: 'Publishing to PRODUCTION. Are you sure?',
initial: false,
});
if (!confirmed) {
throw new Error('Publish cancelled');
}
}
// Require meaningful changelog
if (changelog.some(c => c.toLowerCase().includes('wip'))) {
throw new Error('Remove WIP items from changelog');
}
}Environment-Specific Logic
javascript
beforePublish: async ({ channel }) => {
const { execa } = await import('execa');
if (channel === 'production') {
// Production: Run full test suite
await execa('npm', ['run', 'test:e2e']);
} else {
// Dev/Preview: Quick tests only
await execa('npm', ['run', 'test:unit']);
}
}Error Handling
Hooks can throw errors to stop the publish:
javascript
beforePublish: async () => {
const { execa } = await import('execa');
try {
await execa('npm', ['test']);
} catch (error) {
throw new Error('Tests failed. Fix tests before publishing.');
}
}Async/Await
All hooks support async/await:
javascript
afterPublish: async (version) => {
// Sequential
await sendSlackNotification(version);
await updateChangelog(version);
await gitCommitAndPush();
// Parallel
await Promise.all([
sendSlackNotification(version),
sendDiscordNotification(version),
trackAnalytics(version),
]);
}Best Practices
- Keep Hooks Fast — Don't block publishing unnecessarily
- Handle Errors — Use try/catch for non-critical operations
- Use Environment Variables — For API keys and secrets
- Log Progress — Help debug issues
- Test Hooks — Ensure they work before relying on them