mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
Merge pull request #24466 from overleaf/ls-script-runner
Script runner GitOrigin-RevId: 4cc7004f05177dba2a2151aa6db7e75fb679d11d
This commit is contained in:
@@ -81,6 +81,7 @@ const db = {
|
||||
userAuditLogEntries: internalDb.collection('userAuditLogEntries'),
|
||||
users: internalDb.collection('users'),
|
||||
onboardingDataCollection: internalDb.collection('onboardingDataCollection'),
|
||||
scriptLogs: internalDb.collection('scriptLogs'),
|
||||
}
|
||||
|
||||
const connectionPromise = mongoClient.connect()
|
||||
|
||||
27
services/web/app/src/models/ScriptLog.mjs
Normal file
27
services/web/app/src/models/ScriptLog.mjs
Normal file
@@ -0,0 +1,27 @@
|
||||
import Mongoose from '../infrastructure/Mongoose.js'
|
||||
|
||||
export const ScriptLogSchema = new Mongoose.Schema(
|
||||
{
|
||||
canonicalName: { type: String, required: true },
|
||||
filePathAtVersion: { type: String, required: true },
|
||||
imageVersion: { type: String, required: true },
|
||||
podName: { type: String, required: true },
|
||||
startTime: { type: Date, default: Date.now },
|
||||
endTime: { type: Date, default: null },
|
||||
username: { type: String, required: true },
|
||||
status: {
|
||||
type: String,
|
||||
enum: ['pending', 'success', 'error'],
|
||||
default: 'pending',
|
||||
required: true,
|
||||
},
|
||||
vars: { type: Object, required: true },
|
||||
progressLogs: {
|
||||
type: [{ timestamp: Date, message: String }],
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
{ minimize: false, collection: 'scriptLogs' }
|
||||
)
|
||||
|
||||
export const ScriptLog = Mongoose.model('ScriptLog', ScriptLogSchema)
|
||||
@@ -19,6 +19,7 @@ block append meta
|
||||
- const staffAccess = sessionUser?.staffAccess
|
||||
- const canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || staffAccess?.splitTestMetrics || staffAccess?.splitTestManagement)
|
||||
- const canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
- const canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
- const enableUpgradeButton = projectDashboardReact && usersBestSubscription && (usersBestSubscription.type === 'free' || usersBestSubscription.type === 'standalone-ai-add-on')
|
||||
- const showSignUpLink = hasFeature('registration-page')
|
||||
|
||||
@@ -29,6 +30,7 @@ block append meta
|
||||
canDisplayAdminRedirect,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
enableUpgradeButton,
|
||||
suppressNavbarRight: !!suppressNavbarRight,
|
||||
suppressNavContentLinks: !!suppressNavContentLinks,
|
||||
|
||||
@@ -27,6 +27,7 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
|
||||
- var canDisplayAdminRedirect = canRedirectToAdminDomain()
|
||||
- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
|
||||
- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
- var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
|
||||
if (typeof suppressNavbarRight === "undefined")
|
||||
button.navbar-toggler.collapsed(
|
||||
@@ -66,6 +67,8 @@ nav.navbar.navbar-default.navbar-main.navbar-expand-lg(class={
|
||||
+dropdown-menu-link-item()(href="/admin/split-test") Manage Feature Flags
|
||||
if canDisplaySurveyMenu
|
||||
+dropdown-menu-link-item()(href="/admin/survey") Manage Surveys
|
||||
if canDisplayScriptLogMenu
|
||||
+dropdown-menu-link-item()(href="/admin/script-logs") View Script Logs
|
||||
|
||||
// loop over header_extras
|
||||
each item in nav.header_extras
|
||||
|
||||
@@ -30,6 +30,7 @@ nav.navbar.navbar-default.navbar-main
|
||||
- var canDisplayAdminRedirect = canRedirectToAdminDomain()
|
||||
- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
|
||||
- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
- var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
|
||||
if (typeof(suppressNavbarRight) == "undefined")
|
||||
.navbar-collapse.collapse#navbar-main-collapse
|
||||
@@ -66,6 +67,9 @@ nav.navbar.navbar-default.navbar-main
|
||||
if canDisplaySurveyMenu
|
||||
li
|
||||
a(href="/admin/survey") Manage Surveys
|
||||
if canDisplayScriptLogMenu
|
||||
li
|
||||
a(href="/admin/script-logs") View Script Logs
|
||||
|
||||
// loop over header_extras
|
||||
each item in nav.header_extras
|
||||
|
||||
@@ -30,6 +30,7 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
|
||||
- var canDisplayAdminRedirect = canRedirectToAdminDomain()
|
||||
- var canDisplaySplitTestMenu = hasFeature('saas') && (canDisplayAdminMenu || (getSessionUser() && getSessionUser().staffAccess && (getSessionUser().staffAccess.splitTestMetrics || getSessionUser().staffAccess.splitTestManagement)))
|
||||
- var canDisplaySurveyMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
- var canDisplayScriptLogMenu = hasFeature('saas') && canDisplayAdminMenu
|
||||
|
||||
if (typeof(suppressNavbarRight) == "undefined")
|
||||
.navbar-collapse.collapse#navbar-main-collapse
|
||||
@@ -66,6 +67,9 @@ nav.navbar.navbar-default.navbar-main.website-redesign-navbar
|
||||
if canDisplaySurveyMenu
|
||||
li
|
||||
a(href="/admin/survey") Manage Surveys
|
||||
if canDisplayScriptLogMenu
|
||||
li
|
||||
a(href="/admin/script-logs") View Script Logs
|
||||
|
||||
// loop over header_extras
|
||||
each item in nav.header_extras
|
||||
|
||||
@@ -8,6 +8,7 @@ export default function AdminMenu({
|
||||
canDisplayAdminRedirect,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
adminUrl,
|
||||
}: Pick<
|
||||
DefaultNavbarMetadata,
|
||||
@@ -15,6 +16,7 @@ export default function AdminMenu({
|
||||
| 'canDisplayAdminRedirect'
|
||||
| 'canDisplaySplitTestMenu'
|
||||
| 'canDisplaySurveyMenu'
|
||||
| 'canDisplayScriptLogMenu'
|
||||
| 'adminUrl'
|
||||
>) {
|
||||
const sendProjectListMB = useSendProjectListMB()
|
||||
@@ -57,6 +59,11 @@ export default function AdminMenu({
|
||||
Manage Surveys
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
{canDisplayScriptLogMenu ? (
|
||||
<NavDropdownLinkItem href="/admin/script-logs">
|
||||
View Script Logs
|
||||
</NavDropdownLinkItem>
|
||||
) : null}
|
||||
</NavDropdownMenu>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -22,6 +22,7 @@ function DefaultNavbar(props: DefaultNavbarMetadata) {
|
||||
canDisplayAdminRedirect,
|
||||
canDisplaySplitTestMenu,
|
||||
canDisplaySurveyMenu,
|
||||
canDisplayScriptLogMenu,
|
||||
enableUpgradeButton,
|
||||
suppressNavbarRight,
|
||||
suppressNavContentLinks,
|
||||
@@ -101,6 +102,7 @@ function DefaultNavbar(props: DefaultNavbarMetadata) {
|
||||
canDisplayAdminRedirect={canDisplayAdminRedirect}
|
||||
canDisplaySplitTestMenu={canDisplaySplitTestMenu}
|
||||
canDisplaySurveyMenu={canDisplaySurveyMenu}
|
||||
canDisplayScriptLogMenu={canDisplayScriptLogMenu}
|
||||
adminUrl={adminUrl}
|
||||
/>
|
||||
) : null}
|
||||
|
||||
@@ -10,6 +10,7 @@ export type DefaultNavbarMetadata = {
|
||||
canDisplayAdminRedirect: boolean
|
||||
canDisplaySplitTestMenu: boolean
|
||||
canDisplaySurveyMenu: boolean
|
||||
canDisplayScriptLogMenu: boolean
|
||||
enableUpgradeButton: boolean
|
||||
suppressNavbarRight: boolean
|
||||
suppressNavContentLinks: boolean
|
||||
|
||||
@@ -51,6 +51,7 @@ import { Publisher } from '../../../types/subscription/dashboard/publisher'
|
||||
import { SubscriptionChangePreview } from '../../../types/subscription/subscription-change-preview'
|
||||
import { DefaultNavbarMetadata } from '@/features/ui/components/types/default-navbar-metadata'
|
||||
import { FooterMetadata } from '@/features/ui/components/types/footer-metadata'
|
||||
import type { ScriptLogType } from '../../../modules/admin-panel/frontend/js/features/script-logs/script-log'
|
||||
export interface Meta {
|
||||
'ol-ExposedSettings': ExposedSettings
|
||||
'ol-addonPrices': Record<string, { annual: string; monthly: string }>
|
||||
@@ -162,6 +163,7 @@ export interface Meta {
|
||||
'ol-oauthProviders': OAuthProviders
|
||||
'ol-odcRole': string
|
||||
'ol-overallThemes': OverallThemeMeta[]
|
||||
'ol-pages': number
|
||||
'ol-passwordStrengthOptions': PasswordStrengthOptions
|
||||
'ol-paywallPlans': { [key: string]: string }
|
||||
'ol-personalAccessTokens': AccessToken[] | undefined
|
||||
@@ -188,6 +190,8 @@ export interface Meta {
|
||||
'ol-recurlySubdomain': string
|
||||
'ol-ro-mirror-on-client-no-local-storage': boolean
|
||||
'ol-samlError': SAMLError | undefined
|
||||
'ol-script-log': ScriptLogType
|
||||
'ol-script-logs': ScriptLogType[]
|
||||
'ol-settingsGroupSSO': { enabled: boolean } | undefined
|
||||
'ol-settingsPlans': Plan[]
|
||||
'ol-shouldAllowEditingDetails': boolean
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
/* eslint-disable no-unused-vars */
|
||||
|
||||
import Helpers from './lib/helpers.mjs'
|
||||
|
||||
const tags = ['saas']
|
||||
|
||||
const indexes = [
|
||||
{
|
||||
key: { canonicalName: 1 },
|
||||
name: 'canonicalName_1',
|
||||
},
|
||||
{
|
||||
key: { username: 1 },
|
||||
name: 'username_1',
|
||||
},
|
||||
]
|
||||
|
||||
const migrate = async client => {
|
||||
const { db } = client
|
||||
await Helpers.addIndexesToCollection(db.scriptLogs, indexes)
|
||||
}
|
||||
|
||||
const rollback = async client => {
|
||||
const { db } = client
|
||||
await Helpers.dropIndexesFromCollection(db.scriptLogs, indexes)
|
||||
}
|
||||
|
||||
export default {
|
||||
tags,
|
||||
migrate,
|
||||
rollback,
|
||||
}
|
||||
31
services/web/scripts/example/track_progress.mjs
Normal file
31
services/web/scripts/example/track_progress.mjs
Normal file
@@ -0,0 +1,31 @@
|
||||
// Import the script runner utility (adjust the path as needed)
|
||||
import { scriptRunner } from '../lib/ScriptRunner.mjs'
|
||||
|
||||
const subJobs = 30
|
||||
|
||||
/**
|
||||
* Your script's main work goes here.
|
||||
* It must be an async function and accept `trackProgress`.
|
||||
* @param {(message: string) => Promise<void>} trackProgress - Call this to log progress.
|
||||
*/
|
||||
async function main(trackProgress) {
|
||||
for (let i = 0; i < subJobs; i++) {
|
||||
await new Promise(resolve => setTimeout(() => resolve(), 1000))
|
||||
await trackProgress(`Job in progress ${i + 1}/${subJobs}`)
|
||||
}
|
||||
await trackProgress('Job finished')
|
||||
}
|
||||
|
||||
// Define any variables your script needs (optional)
|
||||
const scriptVariables = {
|
||||
subJobs,
|
||||
}
|
||||
|
||||
// --- Execute the script using the runner with async/await ---
|
||||
try {
|
||||
await scriptRunner(main, scriptVariables)
|
||||
process.exit()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
55
services/web/scripts/lib/README.md
Normal file
55
services/web/scripts/lib/README.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Script Runner
|
||||
|
||||
## Overview
|
||||
|
||||
The Script Runner wraps your script's main logic to automatically handle logging, status tracking (success/error), and progress updates. Script execution status can be viewed from "Script Logs" portal page.
|
||||
|
||||
## Features
|
||||
|
||||
- Automatically logs the start and end of your script.
|
||||
- Records the final status ('success' or 'error').
|
||||
- Provides a simple function (`trackProgress`) to your script for logging custom progress steps.
|
||||
- Captures script parameters and basic environment details.
|
||||
|
||||
## Usage
|
||||
|
||||
1. **Import `scriptRunner`**.
|
||||
2. **Define your script's main logic** as an `async` function that accepts `trackProgress` as its argument (can ignore `trackProgress` if you don't need to track progress).
|
||||
3. **Call `scriptRunner`**, passing your function and any variables it needs.
|
||||
4. **Check script execution status** by visiting the "Script Logs" portal page using the URL printed in the console output.
|
||||
|
||||
**Example:**
|
||||
|
||||
```javascript
|
||||
// Import the script runner utility (adjust the path as needed)
|
||||
import { scriptRunner } from './lib/ScriptRunner.mjs'
|
||||
|
||||
const subJobs = 30
|
||||
|
||||
/**
|
||||
* Your script's main work goes here.
|
||||
* It must be an async function and accept `trackProgress`.
|
||||
* @param {(message: string) => void} trackProgress - Call this to log progress.
|
||||
*/
|
||||
async function main(trackProgress) {
|
||||
for (let i = 0; i < subJobs; i++) {
|
||||
await new Promise(resolve => setTimeout(() => resolve(), 1000))
|
||||
await trackProgress(`Job in progress ${i + 1}/${subJobs}`)
|
||||
}
|
||||
await trackProgress('Job finished')
|
||||
}
|
||||
|
||||
// Define any variables your script needs (optional)
|
||||
const scriptVariables = {
|
||||
subJobs,
|
||||
}
|
||||
|
||||
// --- Execute the script using the runner with async/await ---
|
||||
try {
|
||||
await scriptRunner(main, scriptVariables)
|
||||
process.exit()
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
process.exit(1)
|
||||
}
|
||||
```
|
||||
75
services/web/scripts/lib/ScriptRunner.mjs
Normal file
75
services/web/scripts/lib/ScriptRunner.mjs
Normal file
@@ -0,0 +1,75 @@
|
||||
import { ScriptLog } from '../../app/src/models/ScriptLog.mjs'
|
||||
import Settings from '@overleaf/settings'
|
||||
|
||||
async function beforeScriptExecution(canonicalName, vars, scriptPath) {
|
||||
let log = new ScriptLog({
|
||||
canonicalName,
|
||||
filePathAtVersion: scriptPath,
|
||||
podName: process.env.OL_POD_NAME,
|
||||
username: process.env.OL_USERNAME,
|
||||
imageVersion: process.env.OL_IMAGE_VERSION,
|
||||
vars,
|
||||
})
|
||||
log = await log.save()
|
||||
console.log(
|
||||
'\n==================================' +
|
||||
'\n✨ Your script is running!' +
|
||||
'\n📊 Track progress at:' +
|
||||
`\n${Settings.adminUrl}/admin/script-log/${log._id}` +
|
||||
'\n==================================\n'
|
||||
)
|
||||
return log._id
|
||||
}
|
||||
|
||||
async function afterScriptExecution(logId, status) {
|
||||
await ScriptLog.findByIdAndUpdate(logId, { status, endTime: new Date() })
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {(trackProgress: (progress: string) => Promise<void>) => Promise<any>} main - Main function for the script
|
||||
* @param {Object} vars - Variables to be used in the script
|
||||
* @param {string} canonicalName - The canonical name of the script, default to filename
|
||||
* @param {string} scriptPath - The file path of the script, default to process.argv[1]
|
||||
* @returns {Promise<void>}
|
||||
* @async
|
||||
*/
|
||||
export async function scriptRunner(
|
||||
main,
|
||||
vars = {},
|
||||
canonicalName = process.argv[1].split('/').pop().split('.')[0],
|
||||
scriptPath = process.argv[1]
|
||||
) {
|
||||
const isSaaS = Boolean(Settings.overleaf)
|
||||
if (!isSaaS) {
|
||||
await main(async message => {
|
||||
console.warn(message)
|
||||
})
|
||||
return
|
||||
}
|
||||
const logId = await beforeScriptExecution(canonicalName, vars, scriptPath)
|
||||
|
||||
async function trackProgress(message) {
|
||||
try {
|
||||
console.warn(message)
|
||||
await ScriptLog.findByIdAndUpdate(logId, {
|
||||
$push: {
|
||||
progressLogs: {
|
||||
timestamp: new Date(),
|
||||
message,
|
||||
},
|
||||
},
|
||||
})
|
||||
} catch (error) {
|
||||
console.error('Error tracking progress:', error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await main(trackProgress)
|
||||
} catch (error) {
|
||||
await afterScriptExecution(logId, 'error')
|
||||
throw error
|
||||
}
|
||||
|
||||
await afterScriptExecution(logId, 'success')
|
||||
}
|
||||
Reference in New Issue
Block a user