Skip to content

Update Version Database and Send Notifications #46

Update Version Database and Send Notifications

Update Version Database and Send Notifications #46

name: Update Version Database and Send Notifications
on:
# Run after the build-app-packages workflow completes
workflow_run:
workflows: ["Build App Packages and Collect App Information"]
types:
- completed
# Allow manual triggering
workflow_dispatch:
jobs:
update-database:
runs-on: ubuntu-latest
if: ${{ github.event.workflow_run.conclusion == 'success' || github.event_name == 'workflow_dispatch' }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v3
with:
node-version: "18"
- name: Create package.json and install dependencies
run: |
# Create a minimal package.json file
echo '{"name":"version-tracker","type":"module","private":true}' > package.json
# Install dependencies
npm install @supabase/supabase-js node-fetch
- name: Create version tracking script
run: |
cat > update-versions.js << 'EOF'
import { createClient } from '@supabase/supabase-js';
import fs from 'fs/promises';
import path from 'path';
import fetch from 'node-fetch';
// Configuration
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_KEY = process.env.SUPABASE_KEY; // This must be the service role key, not the anon key
const NOTIFICATIONS_API_URL = process.env.NOTIFICATIONS_API_URL || 'https://intunebrew.com/api/notifications/send';
const NOTIFICATIONS_API_KEY = process.env.NOTIFICATIONS_API_KEY;
// Initialize Supabase client
const supabase = createClient(SUPABASE_URL, SUPABASE_KEY);
// Function to read app JSON files
async function readAppFiles() {
try {
console.log('Reading app JSON files...');
const appsDir = path.join(process.cwd(), 'Apps');
const files = await fs.readdir(appsDir);
const appData = [];
for (const file of files) {
if (file.endsWith('.json')) {
try {
const filePath = path.join(appsDir, file);
const content = await fs.readFile(filePath, 'utf8');
const data = JSON.parse(content);
// Extract app name from filename (remove .json extension)
const appName = file.replace('.json', '');
appData.push({
app_name: appName,
display_name: data.name || data.display_name || appName,
version: data.version || 'unknown',
description: data.description || '',
changelog: data.changelog || '',
homepage: data.homepage || '',
url: data.url || '',
});
} catch (fileError) {
console.error(`Error reading file ${file}:`, fileError);
}
}
}
console.log(`Found ${appData.length} app files`);
return appData;
} catch (error) {
console.error('Error reading app files:', error);
return [];
}
}
// Function to get existing versions from database
async function getExistingVersions() {
try {
console.log('Fetching existing versions from database...');
const { data, error } = await supabase
.from('app_versions')
.select('*');
if (error) {
console.error('Error fetching versions:', error);
return [];
}
console.log(`Found ${data.length} existing version records`);
return data;
} catch (error) {
console.error('Error getting existing versions:', error);
return [];
}
}
// Function to send notification for an app update
async function sendNotification(appName, version, changelog) {
try {
console.log(`Sending notification for ${appName} ${version}...`);
const response = await fetch(NOTIFICATIONS_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${NOTIFICATIONS_API_KEY}`,
'X-Clerk-Secret-Key': process.env.CLERK_SECRET_KEY // Pass the Clerk Secret Key to the API
},
body: JSON.stringify({
appName,
version,
changelog
})
});
const data = await response.json();
if (response.ok) {
console.log(`✅ Notification sent for ${appName} ${version}`);
// Extract the number of users notified from the message if available
const userCountMatch = data.message?.match(/Notification sent to (\d+) of (\d+) users/);
if (userCountMatch) {
console.log(`Notified ${userCountMatch[1]} of ${userCountMatch[2]} subscribed users`);
} else {
console.log(`Summary: ${data.message || 'Notification processed successfully'}`);
}
return true;
} else {
// Handle the case where no users were found in Clerk
if (data.message && data.message.includes("No valid users found with Clerk")) {
console.warn(`⚠️ No valid users found in Clerk for ${appName}. This is expected if running in a GitHub workflow environment without Clerk access.`);
console.warn(`To receive notifications, users need to subscribe in the IntuneBrew website.`);
return true; // Consider this a success since it's an expected limitation
} else {
console.error(`❌ Failed to send notification for ${appName}`);
// Only log the error message, not the full error data
console.error(`Error: ${data.error || 'Unknown error'}`);
return false;
}
}
} catch (error) {
console.error(`Error sending notification for ${appName}:`, error);
return false;
}
}
// Function to send notification for multiple app updates
async function sendBatchNotification(updates) {
try {
if (updates.length === 0) {
console.log('No updates to notify about');
return true;
}
console.log(`Sending batch notification for ${updates.length} app updates...`);
const response = await fetch(NOTIFICATIONS_API_URL, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${NOTIFICATIONS_API_KEY}`,
'x-clerk-secret-key': process.env.CLERK_SECRET_KEY || ''
},
body: JSON.stringify({
updates: updates
})
});
const data = await response.json();
if (response.ok) {
console.log(`✅ Batch notification sent for ${updates.length} app updates`);
// Extract the number of users notified from the message if available
const userCountMatch = data.message?.match(/Notification sent to (\d+) of (\d+) users/);
return true;
} else {
// Handle the case where no users were found in Clerk
if (data.message && data.message.includes("No valid users found with Clerk")) {
console.warn(`⚠️ No valid users found in Clerk. This is expected if running in a GitHub workflow environment without Clerk access.`);
console.warn(`To receive notifications, users need to subscribe in the IntuneBrew website.`);
return true; // Consider this a success since it's an expected limitation
} else {
console.error(`❌ Failed to send batch notification`);
// Only log the error message, not the full error data
console.error(`Error: ${data.error || 'Unknown error'}`);
return false;
}
}
} catch (error) {
console.error(`Error sending batch notification:`, error);
return false;
}
}
// Main function to update versions and send notifications
async function updateVersionsAndNotify() {
try {
// Get current app data from JSON files
const appData = await readAppFiles();
if (appData.length === 0) {
console.log('No app data found, exiting');
return;
}
// Get existing versions from database
const existingVersions = await getExistingVersions();
// Group existing versions by app_name
const versionsByApp = {};
for (const version of existingVersions) {
if (!versionsByApp[version.app_name]) {
versionsByApp[version.app_name] = [];
}
versionsByApp[version.app_name].push(version);
}
// Collect all app updates
const appUpdates = [];
// Process each app
for (const app of appData) {
console.log(`Processing ${app.display_name || app.app_name}...`);
const appVersions = versionsByApp[app.app_name] || [];
// Sort versions by created_at (newest first)
appVersions.sort((a, b) => new Date(b.created_at) - new Date(a.created_at));
// Check if this is a new version
const isNewVersion = appVersions.length === 0 ||
!appVersions.some(v => v.version === app.version);
if (isNewVersion) {
console.log(`New version detected for ${app.app_name}: ${app.version}`);
// Get previous version if available
const previousVersion = appVersions.length > 0 ? appVersions[0].version : null;
// Insert the new version
const { error: insertError } = await supabase
.from('app_versions')
.insert({
app_name: app.app_name,
version: app.version,
display_name: app.display_name,
description: app.description,
changelog: app.changelog,
homepage: app.homepage,
url: app.url,
previous_version: previousVersion
});
if (insertError) {
console.error(`Error inserting version for ${app.app_name}:`, insertError);
continue;
}
console.log(`✅ Added version ${app.version} for ${app.app_name}`);
// If we have a previous version, add to updates list
if (appVersions.length > 0) {
const previousVersion = appVersions[0];
console.log(`Previous version: ${previousVersion.version}`);
// Add to updates list
appUpdates.push({
appName: app.app_name,
version: app.version,
changelog: app.changelog
});
}
// If we now have more than 2 versions, delete the oldest ones
if (appVersions.length >= 2) {
// Keep only the 2 newest versions (the one we just added + the previous one)
const versionsToDelete = appVersions.slice(1); // Skip the most recent one
for (const oldVersion of versionsToDelete) {
console.log(`Deleting old version record: ${app.app_name} ${oldVersion.version}`);
const { error: deleteError } = await supabase
.from('app_versions')
.delete()
.eq('id', oldVersion.id);
if (deleteError) {
console.error(`Error deleting old version for ${app.app_name}:`, deleteError);
}
}
}
} else {
console.log(`No new version for ${app.app_name}, current: ${app.version}`);
// Update the existing version record with any new information
const existingVersion = appVersions.find(v => v.version === app.version);
if (existingVersion) {
const { error: updateError } = await supabase
.from('app_versions')
.update({
display_name: app.display_name,
description: app.description,
changelog: app.changelog,
homepage: app.homepage,
url: app.url,
last_checked: new Date().toISOString()
})
.eq('id', existingVersion.id);
if (updateError) {
console.error(`Error updating version for ${app.app_name}:`, updateError);
}
}
}
}
// Send batch notification for all updates
if (appUpdates.length > 0) {
console.log(`Found ${appUpdates.length} app updates to notify about`);
await sendBatchNotification(appUpdates);
} else {
console.log('No new app updates to notify about');
}
console.log('Version database update completed');
} catch (error) {
console.error('Error in updateVersionsAndNotify:', error);
}
}
// Run the update
updateVersionsAndNotify();
EOF
- name: Run version tracking script
run: node update-versions.js
env:
# Supabase configuration - must use service role key for database operations
SUPABASE_URL: ${{ secrets.SUPABASE_URL }}
SUPABASE_KEY: ${{ secrets.SUPABASE_SERVICE_ROLE_KEY }}
# Notification API configuration
NOTIFICATIONS_API_URL: ${{ secrets.NOTIFICATIONS_API_URL }}
NOTIFICATIONS_API_KEY: ${{ secrets.NOTIFICATIONS_API_KEY }}
# Clerk API configuration
CLERK_SECRET_KEY: ${{ secrets.CLERK_SECRET_KEY }}