feat(api): add bulk trigger workflow server action

Add triggerBulkAddIconWorkflow function that:
- Accepts an array of submission IDs
- Triggers workflows sequentially with 500ms delay
- Returns individual results for each submission
- Handles errors gracefully per-submission
This commit is contained in:
Thomas Camlong
2025-12-29 11:07:52 +01:00
parent 714101680b
commit 91890837e7

View File

@@ -1,48 +1,52 @@
"use server"
import PocketBase from "pocketbase"
import PocketBase from "pocketbase";
const GITHUB_OWNER = "homarr-labs"
const GITHUB_REPO = "dashboard-icons"
const WORKFLOW_FILE = "add-icon.yml"
const GITHUB_OWNER = "homarr-labs";
const GITHUB_REPO = "dashboard-icons";
const WORKFLOW_FILE = "add-icon.yml";
interface TriggerWorkflowResult {
success: boolean
error?: string
workflowUrl?: string
success: boolean;
error?: string;
workflowUrl?: string;
}
/**
* Verify the provided auth token belongs to an admin user
* The token is passed from the client since auth is stored in localStorage
*/
async function verifyAdmin(authToken: string): Promise<{ isAdmin: boolean; error?: string }> {
async function verifyAdmin(
authToken: string,
): Promise<{ isAdmin: boolean; error?: string }> {
if (!authToken) {
return { isAdmin: false, error: "Not authenticated" }
return { isAdmin: false, error: "Not authenticated" };
}
try {
const pb = new PocketBase(process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090")
const pb = new PocketBase(
process.env.NEXT_PUBLIC_POCKETBASE_URL || "http://127.0.0.1:8090",
);
// Validate the token by refreshing auth
// This will fail if the token is invalid/expired
pb.authStore.save(authToken, null)
pb.authStore.save(authToken, null);
const authData = await pb.collection("users").authRefresh()
const authData = await pb.collection("users").authRefresh();
if (!authData?.record) {
return { isAdmin: false, error: "Invalid authentication" }
return { isAdmin: false, error: "Invalid authentication" };
}
// Check if user is admin
if (!authData.record.admin) {
return { isAdmin: false, error: "User is not an admin" }
return { isAdmin: false, error: "User is not an admin" };
}
return { isAdmin: true }
return { isAdmin: true };
} catch (error) {
console.error("Error verifying admin:", error)
return { isAdmin: false, error: "Failed to verify admin status" }
console.error("Error verifying admin:", error);
return { isAdmin: false, error: "Failed to verify admin status" };
}
}
@@ -53,17 +57,21 @@ async function verifyAdmin(authToken: string): Promise<{ isAdmin: boolean; error
* @param submissionId - The ID of the submission to add
* @param dryRun - If true, skip actual writes (for testing)
*/
export async function triggerAddIconWorkflow(authToken: string, submissionId: string, dryRun = false): Promise<TriggerWorkflowResult> {
export async function triggerAddIconWorkflow(
authToken: string,
submissionId: string,
dryRun = false,
): Promise<TriggerWorkflowResult> {
// Verify admin status using the provided token
const { isAdmin, error: authError } = await verifyAdmin(authToken)
const { isAdmin, error: authError } = await verifyAdmin(authToken);
if (!isAdmin) {
return { success: false, error: authError || "Unauthorized" }
return { success: false, error: authError || "Unauthorized" };
}
// Check for GitHub token
const githubToken = process.env.GITHUB_TOKEN
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
return { success: false, error: "GitHub token not configured" }
return { success: false, error: "GitHub token not configured" };
}
try {
@@ -85,30 +93,141 @@ export async function triggerAddIconWorkflow(authToken: string, submissionId: st
},
}),
},
)
);
if (!response.ok) {
const errorText = await response.text()
console.error("GitHub API error:", response.status, errorText)
const errorText = await response.text();
console.error("GitHub API error:", response.status, errorText);
return {
success: false,
error: `GitHub API error: ${response.status} - ${errorText}`,
}
};
}
// The dispatch endpoint returns 204 No Content on success
// Construct a URL to the workflow runs page
const workflowUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/actions/workflows/${WORKFLOW_FILE}`
const workflowUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/actions/workflows/${WORKFLOW_FILE}`;
return {
success: true,
workflowUrl,
}
};
} catch (error) {
console.error("Error triggering workflow:", error)
console.error("Error triggering workflow:", error);
return {
success: false,
error: error instanceof Error ? error.message : "Failed to trigger workflow",
}
error:
error instanceof Error ? error.message : "Failed to trigger workflow",
};
}
}
interface BulkTriggerResult {
submissionId: string;
success: boolean;
error?: string;
}
interface BulkTriggerWorkflowResult {
success: boolean;
results: BulkTriggerResult[];
workflowUrl?: string;
}
/**
* Trigger the "Add Icon to Collection" GitHub workflow for multiple submissions
* Workflows are triggered sequentially with a small delay to avoid rate limiting
* @param authToken - The PocketBase auth token from the client
* @param submissionIds - Array of submission IDs to add
* @param dryRun - If true, skip actual writes (for testing)
*/
export async function triggerBulkAddIconWorkflow(
authToken: string,
submissionIds: string[],
dryRun = false,
): Promise<BulkTriggerWorkflowResult> {
const { isAdmin, error: authError } = await verifyAdmin(authToken);
if (!isAdmin) {
return {
success: false,
results: submissionIds.map((id) => ({
submissionId: id,
success: false,
error: authError || "Unauthorized",
})),
};
}
const githubToken = process.env.GITHUB_TOKEN;
if (!githubToken) {
return {
success: false,
results: submissionIds.map((id) => ({
submissionId: id,
success: false,
error: "GitHub token not configured",
})),
};
}
const results: BulkTriggerResult[] = [];
for (const submissionId of submissionIds) {
try {
const response = await fetch(
`https://api.github.com/repos/${GITHUB_OWNER}/${GITHUB_REPO}/actions/workflows/${WORKFLOW_FILE}/dispatches`,
{
method: "POST",
headers: {
Accept: "application/vnd.github+json",
Authorization: `Bearer ${githubToken}`,
"X-GitHub-Api-Version": "2022-11-28",
},
body: JSON.stringify({
ref: "main",
inputs: {
submissionId: submissionId,
dryRun: dryRun.toString(),
},
}),
},
);
if (!response.ok) {
const errorText = await response.text();
console.error(
`GitHub API error for ${submissionId}:`,
response.status,
errorText,
);
results.push({
submissionId,
success: false,
error: `GitHub API error: ${response.status}`,
});
} else {
results.push({ submissionId, success: true });
}
// Small delay between requests to avoid rate limiting
await new Promise((resolve) => setTimeout(resolve, 500));
} catch (error) {
console.error(`Error triggering workflow for ${submissionId}:`, error);
results.push({
submissionId,
success: false,
error:
error instanceof Error ? error.message : "Failed to trigger workflow",
});
}
}
const allSucceeded = results.every((r) => r.success);
const workflowUrl = `https://github.com/${GITHUB_OWNER}/${GITHUB_REPO}/actions/workflows/${WORKFLOW_FILE}`;
return {
success: allSucceeded,
results,
workflowUrl,
};
}