fix(deploy): Viya deploy checks, startup diagnostics, and chunked deploy script
- Add StartupCheckService to show deploy check progress on loading screen with step-by-step status (appLoc check, Viya deploy, startup service) - Add console logging throughout checkViyaDeploy and viyaMakedataSuccessfull to diagnose deploy flow issues - Fix missing return after resolve(false) in viyaMakedataSuccessfull when folderId is undefined, which caused fall-through to getFolderMembers with an undefined ID - Fix Viya Folders API pagination: getFolderMembers now requests limit=500 so all members are returned (admin folder has 32 members, default page size was hiding makedata) - Fix error handler in viyaMakedataSuccessfull to resolve(false) instead of reject() so the app falls back to setup screen on API errors - Fix licence key whitespace: SAS makedata initialises keys as a single space which is truthy in JS; trim keys before checking so empty keys correctly fall back to free tier instead of triggering decryption errors - Always set _debug=131 on makedata URLs (automatic and manual deploy) so full debug output is available for the one-time setup service - Add makedata completion polling after runMakedataInNewWindow so the app auto-reloads when the makedata job self-deletes, instead of hanging - Fix loading screen slider position from absolute to relative so it does not obscure the startup check steps - Add chunk_deploy.py to sasjs/utils for chunked Viya deploys, splitting viya.sas by service and web file boundaries with correct %let path= tracking per chunk Closes #303, #200, #125
This commit is contained in:
@@ -311,6 +311,45 @@
|
||||
<div class="subline dec"></div>
|
||||
</div>
|
||||
}
|
||||
@if (startupSteps.length > 0) {
|
||||
<div class="startup-checks">
|
||||
@for (step of startupSteps; track step.label) {
|
||||
<div
|
||||
class="startup-check"
|
||||
[class.startup-check--error]="step.status === 'error'"
|
||||
[class.startup-check--done]="step.status === 'done'"
|
||||
>
|
||||
@switch (step.status) {
|
||||
@case ('pending') {
|
||||
<clr-icon
|
||||
shape="circle"
|
||||
class="is-solid startup-check__icon--pending"
|
||||
></clr-icon>
|
||||
}
|
||||
@case ('in_progress') {
|
||||
<clr-spinner clrSmall></clr-spinner>
|
||||
}
|
||||
@case ('done') {
|
||||
<clr-icon
|
||||
shape="check-circle"
|
||||
class="is-solid startup-check__icon--done"
|
||||
></clr-icon>
|
||||
}
|
||||
@case ('error') {
|
||||
<clr-icon
|
||||
shape="exclamation-circle"
|
||||
class="is-solid startup-check__icon--error"
|
||||
></clr-icon>
|
||||
}
|
||||
}
|
||||
<span class="startup-check__label">{{ step.label }}</span>
|
||||
@if (step.detail) {
|
||||
<span class="startup-check__detail">{{ step.detail }}</span>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
<!-- /App Loading Page -->
|
||||
|
||||
@@ -19,6 +19,10 @@ import { InfoModal } from './models/InfoModal'
|
||||
import { DcAdapterSettings } from './models/DcAdapterSettings'
|
||||
import { AppStoreService } from './services/app-store.service'
|
||||
import { LicenceService } from './services/licence.service'
|
||||
import {
|
||||
StartupCheckService,
|
||||
StartupStep
|
||||
} from './services/startup-check.service'
|
||||
import '@cds/core/icon/register.js'
|
||||
import {
|
||||
ClarityIcons,
|
||||
@@ -64,6 +68,7 @@ export class AppComponent {
|
||||
public requestsModal: boolean = false
|
||||
public showRegistration: boolean = true
|
||||
public startupDataLoaded: boolean = false
|
||||
public startupSteps: StartupStep[] = []
|
||||
public demoLimitNotice: { open: boolean; featureName: string } = {
|
||||
open: false,
|
||||
featureName: ''
|
||||
@@ -81,11 +86,16 @@ export class AppComponent {
|
||||
private location: Location,
|
||||
private eventService: EventService,
|
||||
private appStoreService: AppStoreService,
|
||||
private startupCheckService: StartupCheckService,
|
||||
private cdr: ChangeDetectorRef,
|
||||
private elementRef: ElementRef
|
||||
) {
|
||||
this.parseDcAdapterSettings()
|
||||
|
||||
this.startupCheckService.steps$.subscribe((steps) => {
|
||||
this.startupSteps = steps
|
||||
})
|
||||
|
||||
/**
|
||||
* Prints app info in the console such as:
|
||||
* - Adapter versions
|
||||
|
||||
@@ -580,7 +580,9 @@ export class AutomaticComponent implements OnInit {
|
||||
let contextname = `&_contextname=${params.contextName}`
|
||||
let admin = `&admin=${params.admin}`
|
||||
let dcPath = `&dcpath=${params.dcPath}`
|
||||
let debug = this.sasService.getDebugUrlParam()
|
||||
// Debug is ALWAYS enabled for makedata — it runs only once during
|
||||
// deployment and the full log is needed to diagnose any issues.
|
||||
let debug = '&_debug=131'
|
||||
|
||||
let programUrl =
|
||||
serverUrl +
|
||||
@@ -594,6 +596,53 @@ export class AutomaticComponent implements OnInit {
|
||||
debug
|
||||
|
||||
window.open(programUrl)
|
||||
|
||||
// Poll for makedata completion. The makedata service self-deletes
|
||||
// when it finishes successfully, so we check if it's still present
|
||||
// in the admin folder. When it's gone, reload the app fresh.
|
||||
this.pollMakedataCompletion()
|
||||
}
|
||||
|
||||
private makedataPollInterval: any = null
|
||||
|
||||
private pollMakedataCompletion() {
|
||||
if (this.makedataPollInterval) {
|
||||
clearInterval(this.makedataPollInterval)
|
||||
}
|
||||
|
||||
let attempts = 0
|
||||
const maxAttempts = 120 // 10 minutes at 5s intervals
|
||||
|
||||
this.makedataPollInterval = setInterval(async () => {
|
||||
attempts++
|
||||
console.log(
|
||||
`pollMakedataCompletion: checking if makedata is gone (attempt ${attempts}/${maxAttempts})`
|
||||
)
|
||||
|
||||
try {
|
||||
const makedataGone = await this.sasService.viyaMakedataSuccessfull()
|
||||
if (makedataGone) {
|
||||
console.log(
|
||||
'pollMakedataCompletion: makedata job is gone, reloading app'
|
||||
)
|
||||
clearInterval(this.makedataPollInterval)
|
||||
this.makedataPollInterval = null
|
||||
|
||||
// Reload the app fresh from the SASJobExecution URL
|
||||
window.location.href = window.location.href.split('#')[0]
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('pollMakedataCompletion: error checking', err)
|
||||
}
|
||||
|
||||
if (attempts >= maxAttempts) {
|
||||
console.warn(
|
||||
'pollMakedataCompletion: timed out after 10 minutes, stopping poll'
|
||||
)
|
||||
clearInterval(this.makedataPollInterval)
|
||||
this.makedataPollInterval = null
|
||||
}
|
||||
}, 5000)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -242,6 +242,9 @@ export class ManualComponent implements OnInit {
|
||||
*/
|
||||
public createDatabase(newTab: boolean = true) {
|
||||
if (newTab) {
|
||||
// Debug is ALWAYS enabled for makedata — it runs only once during
|
||||
// deployment and the full log is needed to diagnose any issues.
|
||||
const _debug = '&_debug=131'
|
||||
let url =
|
||||
this.sasService.getSasjsConfig().serverUrl +
|
||||
'/SASJobExecution/?_program=' +
|
||||
@@ -252,7 +255,7 @@ export class ManualComponent implements OnInit {
|
||||
this.selectedAdminGroup +
|
||||
'&DCPATH=' +
|
||||
this.dcPath +
|
||||
this.sasService.getDebugUrlParam()
|
||||
_debug
|
||||
|
||||
window.open(url, '_blank')
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import { AppSettingsService } from './app-settings.service'
|
||||
import { AppThemes } from '../models/AppSettings'
|
||||
import { RequestWrapperResponse } from '../models/request-wrapper/RequestWrapperResponse'
|
||||
import { AppStoreService } from './app-store.service'
|
||||
import { StartupCheckService } from './startup-check.service'
|
||||
import { retryOnce, RetryOnceOptions } from '../shared/utils/retry-once'
|
||||
import { getMalformedAdapterResponseMessage } from '../shared/utils/get-malformed-adapter-response-message'
|
||||
|
||||
@@ -29,7 +30,8 @@ export class AppService {
|
||||
private loggerService: LoggerService,
|
||||
private appSettingsService: AppSettingsService,
|
||||
private router: Router,
|
||||
private appStoreService: AppStoreService
|
||||
private appStoreService: AppStoreService,
|
||||
private startupCheckService: StartupCheckService
|
||||
) {
|
||||
this.subscribe()
|
||||
|
||||
@@ -100,6 +102,11 @@ export class AppService {
|
||||
)
|
||||
if (malformedMessage) {
|
||||
startupServiceError = true
|
||||
console.error(
|
||||
'startUpData: startupservice returned malformed response:',
|
||||
malformedMessage
|
||||
)
|
||||
this.startupCheckService.updateStep(2, 'error', 'Malformed response')
|
||||
this.eventService.showInfoModal('Error', malformedMessage)
|
||||
this.licenceService.isAppActivated.next(false)
|
||||
|
||||
@@ -121,6 +128,15 @@ export class AppService {
|
||||
|
||||
if (missingProps.length > 0) {
|
||||
startupServiceError = true
|
||||
console.error(
|
||||
'startUpData: startupservice missing properties:',
|
||||
missingProps.join(', ')
|
||||
)
|
||||
this.startupCheckService.updateStep(
|
||||
2,
|
||||
'error',
|
||||
`Missing: ${missingProps.join(', ')}`
|
||||
)
|
||||
this.eventService.showInfoModal(
|
||||
'Error',
|
||||
`${missingProps.join(', ')} are not present in the startupservice`
|
||||
@@ -198,6 +214,8 @@ export class AppService {
|
||||
})
|
||||
.catch((err: any) => {
|
||||
startupServiceError = true
|
||||
console.error('startUpData: startupservice request failed:', err)
|
||||
this.startupCheckService.updateStep(2, 'error', 'Request failed')
|
||||
this.eventService.showInfoModal(
|
||||
'Error',
|
||||
'There is an issue with startupservice response'
|
||||
|
||||
@@ -117,6 +117,13 @@ export class LicenceService {
|
||||
|
||||
let variables = globvars[0]
|
||||
|
||||
// Trim whitespace — the SAS makedata service initialises licence keys
|
||||
// as a single space, which is truthy in JS but not a valid key.
|
||||
if (variables.LICENCE_KEY)
|
||||
variables.LICENCE_KEY = variables.LICENCE_KEY.trim()
|
||||
if (variables.ACTIVATION_KEY)
|
||||
variables.ACTIVATION_KEY = variables.ACTIVATION_KEY.trim()
|
||||
|
||||
if (
|
||||
variables.LICENCE_KEY === undefined ||
|
||||
variables.ACTIVATION_KEY === undefined ||
|
||||
|
||||
@@ -150,9 +150,12 @@ export class SasViyaService {
|
||||
)
|
||||
}
|
||||
|
||||
getFolderMembers(folderId: string): Observable<ViyaApiFolderMembers> {
|
||||
getFolderMembers(
|
||||
folderId: string,
|
||||
limit: number = 500
|
||||
): Observable<ViyaApiFolderMembers> {
|
||||
return this.get<ViyaApiFolderMembers>(
|
||||
`${this.serverUrl}/folders/folders/${folderId}/members`,
|
||||
`${this.serverUrl}/folders/folders/${folderId}/members?limit=${limit}`,
|
||||
{
|
||||
withCredentials: true
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { Injectable, EventEmitter } from '@angular/core'
|
||||
import SASjs, { UploadFile } from '@sasjs/adapter'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
import { UserService } from '../shared/user.service'
|
||||
import { StartupCheckService } from './startup-check.service'
|
||||
|
||||
import { Router } from '@angular/router'
|
||||
import { EventService } from './event.service'
|
||||
@@ -43,6 +44,7 @@ export class SasService {
|
||||
private sasjsService: SasjsService,
|
||||
private sasViyaService: SasViyaService,
|
||||
private loggerService: LoggerService,
|
||||
private startupCheckService: StartupCheckService,
|
||||
private router: Router
|
||||
) {}
|
||||
|
||||
@@ -437,6 +439,9 @@ export class SasService {
|
||||
}
|
||||
|
||||
public async checkViyaDeploy(path: string) {
|
||||
console.log('checkViyaDeploy: checking appLoc', path)
|
||||
this.startupCheckService.updateStep(0, 'in_progress')
|
||||
|
||||
const getFolderExistsInAdapter =
|
||||
typeof this.sasjsAdapter.getFolder !== 'undefined'
|
||||
|
||||
@@ -452,25 +457,58 @@ export class SasService {
|
||||
appLocExists = await this.appLocCheckPreAxiosdAdapter(path)
|
||||
}
|
||||
|
||||
console.log(
|
||||
'checkViyaDeploy: appLoc exists?',
|
||||
appLocExists,
|
||||
'error?',
|
||||
errorMessage
|
||||
)
|
||||
|
||||
if (appLocExists) {
|
||||
this.startupCheckService.updateStep(0, 'done', path)
|
||||
|
||||
// Check if there is appLoc/services/admin/makedata.sas present
|
||||
// if yes, it needs to be run, so we redirect to /deploy
|
||||
// if not, we load the startup service
|
||||
|
||||
this.startupCheckService.updateStep(1, 'in_progress')
|
||||
this.viyaMakedataSuccessfull().then(
|
||||
(success: boolean) => {
|
||||
console.log(
|
||||
'checkViyaDeploy: makedata job already run?',
|
||||
success,
|
||||
success
|
||||
? '-> loading startupservice'
|
||||
: '-> redirecting to /deploy (setup screen)'
|
||||
)
|
||||
if (success) {
|
||||
this.startupCheckService.updateStep(1, 'done', 'makedata complete')
|
||||
this.startupCheckService.updateStep(2, 'in_progress')
|
||||
this.loadStartupServiceEmitter.emit()
|
||||
} else {
|
||||
this.startupCheckService.updateStep(
|
||||
1,
|
||||
'done',
|
||||
'makedata job found - setup needed'
|
||||
)
|
||||
this.eventService.startupDataLoaded()
|
||||
this.router.navigateByUrl('/deploy')
|
||||
}
|
||||
},
|
||||
(error: any) => {
|
||||
console.error('Error while looking for the file: makedata.sas', error)
|
||||
console.error(
|
||||
'checkViyaDeploy: error while looking for makedata job',
|
||||
error
|
||||
)
|
||||
this.startupCheckService.updateStep(1, 'error', String(error))
|
||||
}
|
||||
)
|
||||
} else {
|
||||
this.startupCheckService.updateStep(
|
||||
0,
|
||||
'error',
|
||||
errorMessage || 'not found'
|
||||
)
|
||||
const errorMessageToShow =
|
||||
(errorMessage ||
|
||||
'Viya services are not present on the current appLoc, or API not reachable. Check the ADAPTER configuration.') +
|
||||
@@ -480,43 +518,71 @@ export class SasService {
|
||||
}
|
||||
}
|
||||
|
||||
private async viyaMakedataSuccessfull(): Promise<boolean> {
|
||||
public async viyaMakedataSuccessfull(): Promise<boolean> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const sasjsConfig = this.getSasjsConfig()
|
||||
const configuratorFolder = `${sasjsConfig.appLoc}/services/admin`
|
||||
|
||||
console.log(
|
||||
'viyaMakedataSuccessfull: checking folder',
|
||||
configuratorFolder
|
||||
)
|
||||
|
||||
this.sasViyaService.getFolderByPath(configuratorFolder).subscribe(
|
||||
(folderInfo: ViyaApiFolder) => {
|
||||
const folderId = folderInfo.id
|
||||
|
||||
if (!folderId) {
|
||||
console.error(
|
||||
`Folder ID is not present. ${configuratorFolder}`,
|
||||
`viyaMakedataSuccessfull: folder ID not present for ${configuratorFolder}`,
|
||||
sasjsConfig
|
||||
)
|
||||
resolve(false)
|
||||
return
|
||||
}
|
||||
|
||||
this.sasViyaService.getFolderMembers(folderId).subscribe(
|
||||
(members: ViyaApiFolderMembers) => {
|
||||
if (
|
||||
!members.items.some((item: any) => item.name === 'makedata')
|
||||
) {
|
||||
// Makedata.sas is not present, which means it was run
|
||||
const memberNames = members.items.map((item: any) => item.name)
|
||||
const hasMakedata = members.items.some(
|
||||
(item: any) => item.name === 'makedata'
|
||||
)
|
||||
|
||||
console.log(
|
||||
'viyaMakedataSuccessfull: folder members:',
|
||||
memberNames.join(', ') || '(none)'
|
||||
)
|
||||
console.log(
|
||||
'viyaMakedataSuccessfull: makedata job present?',
|
||||
hasMakedata,
|
||||
hasMakedata
|
||||
? '-> setup needed (redirect to /deploy)'
|
||||
: '-> setup already done (load startupservice)'
|
||||
)
|
||||
|
||||
if (!hasMakedata) {
|
||||
resolve(true)
|
||||
} else {
|
||||
// Makedata.sas is present, which means it was not run
|
||||
resolve(false)
|
||||
}
|
||||
},
|
||||
(err: any) => {
|
||||
console.error('Error getting folder contents', err)
|
||||
reject()
|
||||
console.error(
|
||||
'viyaMakedataSuccessfull: error getting folder members for',
|
||||
configuratorFolder,
|
||||
err
|
||||
)
|
||||
// On error, assume setup is needed rather than skipping it
|
||||
resolve(false)
|
||||
}
|
||||
)
|
||||
},
|
||||
(err: any) => {
|
||||
console.warn('Error getting folder info', err)
|
||||
console.warn(
|
||||
'viyaMakedataSuccessfull: error getting folder info for',
|
||||
configuratorFolder,
|
||||
err
|
||||
)
|
||||
reject(err)
|
||||
}
|
||||
)
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { Injectable } from '@angular/core'
|
||||
import { BehaviorSubject } from 'rxjs'
|
||||
|
||||
export type StartupStepStatus = 'pending' | 'in_progress' | 'done' | 'error'
|
||||
|
||||
export interface StartupStep {
|
||||
label: string
|
||||
status: StartupStepStatus
|
||||
detail?: string
|
||||
}
|
||||
|
||||
@Injectable({ providedIn: 'root' })
|
||||
export class StartupCheckService {
|
||||
private steps: StartupStep[] = [
|
||||
{ label: 'Checking app location', status: 'pending' },
|
||||
{ label: 'Checking Viya deploy', status: 'pending' },
|
||||
{ label: 'Loading startup service', status: 'pending' }
|
||||
]
|
||||
|
||||
public steps$ = new BehaviorSubject<StartupStep[]>([...this.steps])
|
||||
|
||||
public updateStep(index: number, status: StartupStepStatus, detail?: string) {
|
||||
if (index >= 0 && index < this.steps.length) {
|
||||
this.steps[index] = {
|
||||
...this.steps[index],
|
||||
status,
|
||||
detail: detail ?? this.steps[index].detail
|
||||
}
|
||||
this.steps$.next([...this.steps])
|
||||
}
|
||||
}
|
||||
|
||||
public setSteps(steps: StartupStep[]) {
|
||||
this.steps = steps
|
||||
this.steps$.next([...this.steps])
|
||||
}
|
||||
}
|
||||
+52
-3
@@ -4189,10 +4189,10 @@ body[cds-theme='light'] {
|
||||
|
||||
// Custom loading spinner
|
||||
.slider {
|
||||
position: absolute;
|
||||
position: relative;
|
||||
width: 320px;
|
||||
margin-left: 75px;
|
||||
margin-top: 70px;
|
||||
margin-left: 0;
|
||||
margin-top: 20px;
|
||||
height: 5px;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
@@ -4259,6 +4259,55 @@ body[cds-theme='light'] {
|
||||
bottom: 0;
|
||||
}
|
||||
|
||||
.startup-checks {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
margin-top: 24px;
|
||||
max-width: 420px;
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.startup-check {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: #666;
|
||||
|
||||
&--done {
|
||||
color: #3c8500;
|
||||
}
|
||||
|
||||
&--error {
|
||||
color: #c21d00;
|
||||
}
|
||||
|
||||
&__icon--pending {
|
||||
color: #999;
|
||||
}
|
||||
|
||||
&__icon--done {
|
||||
color: #3c8500;
|
||||
}
|
||||
|
||||
&__icon--error {
|
||||
color: #c21d00;
|
||||
}
|
||||
|
||||
&__label {
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
&__detail {
|
||||
color: #999;
|
||||
font-size: 12px;
|
||||
margin-left: auto;
|
||||
text-align: right;
|
||||
word-break: break-word;
|
||||
}
|
||||
}
|
||||
|
||||
.select-none {
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
Generated
+70
-70
@@ -1075,9 +1075,9 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/brace-expansion": {
|
||||
"version": "1.1.14",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz",
|
||||
"integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==",
|
||||
"version": "1.1.18",
|
||||
"resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz",
|
||||
"integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -1721,16 +1721,16 @@
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.4.tgz",
|
||||
"integrity": "sha512-f0cRzm6dkyVYV3nPoooP8XlccPQukegwhAnpoLcXy+X+A8KfpGOoXwDr9FLZd3wzgLaBGQBE3lY93Zm/i1JvIQ==",
|
||||
"version": "3.0.5",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.5.tgz",
|
||||
"integrity": "sha512-j23EibVLnp4zNXGW7LjryXYa2X6U/M96yoOX+ybZxwkYajdxRNEqYY3zhh7y0i6kfISKS2jr+EJq1YTUDEv5+w==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"es-set-tostringtag": "^2.1.0",
|
||||
"hasown": "^2.0.2",
|
||||
"hasown": "^2.0.4",
|
||||
"mime-types": "^2.1.35"
|
||||
},
|
||||
"engines": {
|
||||
@@ -1992,9 +1992,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/hasown": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
|
||||
"integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
|
||||
"version": "2.0.4",
|
||||
"resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz",
|
||||
"integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
@@ -2385,9 +2385,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm": {
|
||||
"version": "11.13.0",
|
||||
"resolved": "https://registry.npmjs.org/npm/-/npm-11.13.0.tgz",
|
||||
"integrity": "sha512-cRmhaghDWA1lFgl3Ug4/VxDJdPBK/U+tNtnrl9kXunFqhWw1x4xL5txkNn7qzPuVfvXOmXyjHpMwsuk2uisbkg==",
|
||||
"version": "11.19.0",
|
||||
"resolved": "https://registry.npmjs.org/npm/-/npm-11.19.0.tgz",
|
||||
"integrity": "sha512-SDd/hHg3KqHE5Ht2NHWxNYNtqCQ2pXAPLl6OtQhPyED5PHsRfrOtO199MZTIG2cQoQ1ZRI9t28shrD+2cr3AAw==",
|
||||
"bundleDependencies": [
|
||||
"@isaacs/string-locale-compare",
|
||||
"@npmcli/arborist",
|
||||
@@ -2466,8 +2466,8 @@
|
||||
],
|
||||
"dependencies": {
|
||||
"@isaacs/string-locale-compare": "^1.1.0",
|
||||
"@npmcli/arborist": "^9.4.3",
|
||||
"@npmcli/config": "^10.8.1",
|
||||
"@npmcli/arborist": "^9.9.1",
|
||||
"@npmcli/config": "^10.12.0",
|
||||
"@npmcli/fs": "^5.0.0",
|
||||
"@npmcli/map-workspaces": "^5.0.3",
|
||||
"@npmcli/metavuln-calculator": "^9.0.3",
|
||||
@@ -2485,46 +2485,46 @@
|
||||
"fs-minipass": "^3.0.3",
|
||||
"glob": "^13.0.6",
|
||||
"graceful-fs": "^4.2.11",
|
||||
"hosted-git-info": "^9.0.2",
|
||||
"hosted-git-info": "^9.0.3",
|
||||
"ini": "^6.0.0",
|
||||
"init-package-json": "^8.2.5",
|
||||
"is-cidr": "^6.0.4",
|
||||
"json-parse-even-better-errors": "^5.0.0",
|
||||
"libnpmaccess": "^10.0.3",
|
||||
"libnpmdiff": "^8.1.6",
|
||||
"libnpmexec": "^10.2.6",
|
||||
"libnpmfund": "^7.0.20",
|
||||
"libnpmdiff": "^8.1.12",
|
||||
"libnpmexec": "^10.3.2",
|
||||
"libnpmfund": "^7.0.26",
|
||||
"libnpmorg": "^8.0.1",
|
||||
"libnpmpack": "^9.1.6",
|
||||
"libnpmpublish": "^11.1.3",
|
||||
"libnpmpack": "^9.1.12",
|
||||
"libnpmpublish": "^11.2.0",
|
||||
"libnpmsearch": "^9.0.1",
|
||||
"libnpmteam": "^8.0.2",
|
||||
"libnpmversion": "^8.0.3",
|
||||
"make-fetch-happen": "^15.0.5",
|
||||
"libnpmversion": "^8.0.4",
|
||||
"make-fetch-happen": "^15.0.6",
|
||||
"minimatch": "^10.2.5",
|
||||
"minipass": "^7.1.3",
|
||||
"minipass-pipeline": "^1.2.4",
|
||||
"ms": "^2.1.2",
|
||||
"node-gyp": "^12.3.0",
|
||||
"node-gyp": "^12.4.0",
|
||||
"nopt": "^9.0.0",
|
||||
"npm-audit-report": "^7.0.0",
|
||||
"npm-install-checks": "^8.0.0",
|
||||
"npm-package-arg": "^13.0.2",
|
||||
"npm-pick-manifest": "^11.0.3",
|
||||
"npm-profile": "^12.0.1",
|
||||
"npm-profile": "^12.0.2",
|
||||
"npm-registry-fetch": "^19.1.1",
|
||||
"npm-user-validate": "^4.0.0",
|
||||
"p-map": "^7.0.4",
|
||||
"pacote": "^21.5.0",
|
||||
"pacote": "^21.5.1",
|
||||
"parse-conflict-json": "^5.0.1",
|
||||
"proc-log": "^6.1.0",
|
||||
"qrcode-terminal": "^0.12.0",
|
||||
"read": "^5.0.1",
|
||||
"semver": "^7.7.4",
|
||||
"semver": "^7.8.5",
|
||||
"spdx-expression-parse": "^4.0.0",
|
||||
"ssri": "^13.0.1",
|
||||
"supports-color": "^10.2.2",
|
||||
"tar": "^7.5.13",
|
||||
"tar": "^7.5.19",
|
||||
"text-table": "~0.2.0",
|
||||
"tiny-relative-date": "^2.0.2",
|
||||
"treeverse": "^3.0.0",
|
||||
@@ -2580,7 +2580,7 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/npm/node_modules/@npmcli/agent": {
|
||||
"version": "4.0.0",
|
||||
"version": "4.0.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -2596,7 +2596,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@npmcli/arborist": {
|
||||
"version": "9.4.3",
|
||||
"version": "9.9.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -2644,7 +2644,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@npmcli/config": {
|
||||
"version": "10.8.1",
|
||||
"version": "10.12.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -2838,7 +2838,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@sigstore/core": {
|
||||
"version": "3.2.0",
|
||||
"version": "3.2.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
@@ -2886,13 +2886,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/@sigstore/verify": {
|
||||
"version": "3.1.0",
|
||||
"version": "3.1.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/bundle": "^4.0.0",
|
||||
"@sigstore/core": "^3.1.0",
|
||||
"@sigstore/core": "^3.2.1",
|
||||
"@sigstore/protobuf-specs": "^0.5.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -2961,7 +2961,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/bin-links": {
|
||||
"version": "6.0.0",
|
||||
"version": "6.0.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -2989,7 +2989,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/brace-expansion": {
|
||||
"version": "5.0.5",
|
||||
"version": "5.0.7",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -3058,7 +3058,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/cidr-regex": {
|
||||
"version": "5.0.4",
|
||||
"version": "5.0.5",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "BSD-2-Clause",
|
||||
@@ -3182,7 +3182,7 @@
|
||||
"license": "ISC"
|
||||
},
|
||||
"node_modules/npm/node_modules/hosted-git-info": {
|
||||
"version": "9.0.2",
|
||||
"version": "9.0.3",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -3281,7 +3281,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/ip-address": {
|
||||
"version": "10.1.0",
|
||||
"version": "10.2.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -3363,12 +3363,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmdiff": {
|
||||
"version": "8.1.6",
|
||||
"version": "8.1.12",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^9.4.3",
|
||||
"@npmcli/arborist": "^9.9.1",
|
||||
"@npmcli/installed-package-contents": "^4.0.0",
|
||||
"binary-extensions": "^3.0.0",
|
||||
"diff": "^8.0.2",
|
||||
@@ -3382,13 +3382,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmexec": {
|
||||
"version": "10.2.6",
|
||||
"version": "10.3.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@gar/promise-retry": "^1.0.0",
|
||||
"@npmcli/arborist": "^9.4.3",
|
||||
"@npmcli/arborist": "^9.9.1",
|
||||
"@npmcli/package-json": "^7.0.0",
|
||||
"@npmcli/run-script": "^10.0.0",
|
||||
"ci-info": "^4.0.0",
|
||||
@@ -3405,12 +3405,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmfund": {
|
||||
"version": "7.0.20",
|
||||
"version": "7.0.26",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^9.4.3"
|
||||
"@npmcli/arborist": "^9.9.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
@@ -3430,12 +3430,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmpack": {
|
||||
"version": "9.1.6",
|
||||
"version": "9.1.12",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@npmcli/arborist": "^9.4.3",
|
||||
"@npmcli/arborist": "^9.9.1",
|
||||
"@npmcli/run-script": "^10.0.0",
|
||||
"npm-package-arg": "^13.0.0",
|
||||
"pacote": "^21.0.2"
|
||||
@@ -3445,7 +3445,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmpublish": {
|
||||
"version": "11.1.3",
|
||||
"version": "11.2.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -3489,7 +3489,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/libnpmversion": {
|
||||
"version": "8.0.3",
|
||||
"version": "8.0.4",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -3505,7 +3505,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/lru-cache": {
|
||||
"version": "11.3.5",
|
||||
"version": "11.5.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
@@ -3514,7 +3514,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/make-fetch-happen": {
|
||||
"version": "15.0.5",
|
||||
"version": "15.0.6",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -3680,7 +3680,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/node-gyp": {
|
||||
"version": "12.3.0",
|
||||
"version": "12.4.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -3804,13 +3804,13 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/npm-profile": {
|
||||
"version": "12.0.1",
|
||||
"version": "12.0.2",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"npm-registry-fetch": "^19.0.0",
|
||||
"proc-log": "^6.0.0"
|
||||
"proc-log": "^6.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
@@ -3857,7 +3857,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/pacote": {
|
||||
"version": "21.5.0",
|
||||
"version": "21.5.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -3918,7 +3918,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/postcss-selector-parser": {
|
||||
"version": "7.1.1",
|
||||
"version": "7.1.4",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -4015,7 +4015,7 @@
|
||||
"optional": true
|
||||
},
|
||||
"node_modules/npm/node_modules/semver": {
|
||||
"version": "7.7.4",
|
||||
"version": "7.8.5",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "ISC",
|
||||
@@ -4039,17 +4039,17 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/sigstore": {
|
||||
"version": "4.1.0",
|
||||
"version": "4.1.1",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@sigstore/bundle": "^4.0.0",
|
||||
"@sigstore/core": "^3.1.0",
|
||||
"@sigstore/core": "^3.2.1",
|
||||
"@sigstore/protobuf-specs": "^0.5.0",
|
||||
"@sigstore/sign": "^4.1.0",
|
||||
"@sigstore/tuf": "^4.0.1",
|
||||
"@sigstore/verify": "^3.1.0"
|
||||
"@sigstore/sign": "^4.1.1",
|
||||
"@sigstore/tuf": "^4.0.2",
|
||||
"@sigstore/verify": "^3.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": "^20.17.0 || >=22.9.0"
|
||||
@@ -4066,12 +4066,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/socks": {
|
||||
"version": "2.8.7",
|
||||
"version": "2.8.9",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"ip-address": "^10.0.1",
|
||||
"ip-address": "^10.1.1",
|
||||
"smart-buffer": "^4.2.0"
|
||||
},
|
||||
"engines": {
|
||||
@@ -4140,7 +4140,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/tar": {
|
||||
"version": "7.5.13",
|
||||
"version": "7.5.19",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "BlueOak-1.0.0",
|
||||
@@ -4168,7 +4168,7 @@
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/npm/node_modules/tinyglobby": {
|
||||
"version": "0.2.16",
|
||||
"version": "0.2.17",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -4236,7 +4236,7 @@
|
||||
}
|
||||
},
|
||||
"node_modules/npm/node_modules/undici": {
|
||||
"version": "6.25.0",
|
||||
"version": "6.27.0",
|
||||
"dev": true,
|
||||
"inBundle": true,
|
||||
"license": "MIT",
|
||||
@@ -4951,9 +4951,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.25.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.25.0.tgz",
|
||||
"integrity": "sha512-ZgpWDC5gmNiuY9CnLVXEH8rl50xhRCuLNA97fAUnKi8RRuV4E6KG31pDTsLVUKnohJE0I3XDrTeEydAXRw47xg==",
|
||||
"version": "6.28.0",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz",
|
||||
"integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
|
||||
Generated
+11
-11
@@ -6,7 +6,7 @@
|
||||
"": {
|
||||
"name": "dc-sas",
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "4.19.0",
|
||||
"@sasjs/cli": "4.20.1",
|
||||
"@sasjs/core": "5.1.0"
|
||||
}
|
||||
},
|
||||
@@ -200,9 +200,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/adapter": {
|
||||
"version": "4.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.18.0.tgz",
|
||||
"integrity": "sha512-1gP0Rsl2cRAMSchjCIrDkF+T6WA/fPXGIB+ABAXWCQxr6gM2JXqthebL+GxwDsq0yCoEp4w47KplaEkdBDy/EQ==",
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/adapter/-/adapter-4.19.0.tgz",
|
||||
"integrity": "sha512-AhSldduDFPAuhRsXdzeiwMqEm0lm6dnh2yVxDSUmxxw8V9YShmeGPmEzvU8n2Cl4VAyrhc+Ietc5YWFYC03G+Q==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/utils": "^3.6.0",
|
||||
@@ -214,12 +214,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/cli": {
|
||||
"version": "4.19.0",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.19.0.tgz",
|
||||
"integrity": "sha512-AVerY31E0+BdprT42ZyG2FKb/aYQLwXvAiPP0hqT4mW9B8N3vSJKkR+8yNBfNtyZb7EyRj7GRf2mc2eZkQL/NA==",
|
||||
"version": "4.20.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/cli/-/cli-4.20.1.tgz",
|
||||
"integrity": "sha512-482xPlEuyBKqGlg2ArCiREu3Q9+8TsTj0dwy5ewNURi7pWEcwJH0A4c9Yw4a3akUtPAphPEzs8/9BkcS/v+3WQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@sasjs/adapter": "^4.18.0",
|
||||
"@sasjs/adapter": "^4.19.0",
|
||||
"@sasjs/core": "4.68.1",
|
||||
"@sasjs/lint": "2.4.3",
|
||||
"@sasjs/utils": "^3.6.0",
|
||||
@@ -305,9 +305,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@sasjs/utils": {
|
||||
"version": "3.6.1",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.6.1.tgz",
|
||||
"integrity": "sha512-Oyi3nkmhi9IjV7hiZhiqRplJspjQqIA7XT6SSBp+nsXLFeHfQacKJ8tqqCUkzcz7MtG2/ifact64zj4voAsBdg==",
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@sasjs/utils/-/utils-3.6.2.tgz",
|
||||
"integrity": "sha512-AfC5OJuJx3NAbntKq2J8E8NNpRXthYfuqOyp2lOmOkTctL3mAfrA54U/cdPQvrlftXNxpK9P2U0RU10qNjlEzQ==",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@fast-csv/format": "4.3.5",
|
||||
|
||||
+1
-1
@@ -30,7 +30,7 @@
|
||||
},
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"@sasjs/cli": "4.19.0",
|
||||
"@sasjs/cli": "4.20.1",
|
||||
"@sasjs/core": "5.1.0"
|
||||
},
|
||||
"overrides": {
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
# Resolve paths relative to the repo root (sas/ is two levels up from sasjs/utils/)
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
SAS_DIR = os.path.normpath(os.path.join(SCRIPT_DIR, '..', '..'))
|
||||
VFILE = os.path.join(SAS_DIR, 'sasjsbuild', 'viya.sas')
|
||||
CHUNK_DIR = os.path.join(SAS_DIR, 'sasjsbuild', 'chunks')
|
||||
|
||||
with open(VFILE) as f:
|
||||
lines = f.readlines()
|
||||
|
||||
# Find the header end (first %let path=services or %let service=)
|
||||
HEADER_END = None
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith('%let path=services') or line.startswith('%let service='):
|
||||
HEADER_END = i
|
||||
break
|
||||
|
||||
header = lines[:HEADER_END]
|
||||
|
||||
# Parse all %let path= and %let service= / %let filename= declarations
|
||||
# to build chunks that each start with the correct %let path=
|
||||
sections = [] # list of (start, end, path_line_text)
|
||||
current_path = None
|
||||
current_start = None
|
||||
|
||||
i = HEADER_END
|
||||
while i < len(lines):
|
||||
line = lines[i]
|
||||
if line.startswith('%let path='):
|
||||
# Close previous section
|
||||
if current_start is not None:
|
||||
sections.append((current_start, i, current_path))
|
||||
current_path = line
|
||||
current_start = i
|
||||
elif line.startswith('%let service=') or line.startswith('%let filename='):
|
||||
if current_start is not None:
|
||||
sections.append((current_start, i, current_path))
|
||||
current_start = i
|
||||
# Don't update current_path — it stays from the last %let path=
|
||||
i += 1
|
||||
if current_start is not None:
|
||||
sections.append((current_start, len(lines), current_path))
|
||||
|
||||
print(f'Total lines: {len(lines)}, header: {len(header)}, sections: {len(sections)}')
|
||||
|
||||
# Group sections into chunks
|
||||
# Service sections: 10 per chunk
|
||||
# Web file sections: 2 per chunk (1 if large)
|
||||
# Image sections: 1 per chunk
|
||||
# Each chunk gets the header + its %let path= prepended
|
||||
|
||||
os.makedirs(CHUNK_DIR, exist_ok=True)
|
||||
for old in os.listdir(CHUNK_DIR):
|
||||
os.remove(os.path.join(CHUNK_DIR, old))
|
||||
|
||||
chunk_files = []
|
||||
|
||||
# Group services together
|
||||
service_sections = [s for s in sections if lines[s[0]].startswith('%let service=')]
|
||||
web_sections = [s for s in sections if lines[s[0]].startswith('%let filename=') and s not in service_sections]
|
||||
# Separate web/images sections
|
||||
web_file_sections = []
|
||||
images_sections = []
|
||||
for s in sections:
|
||||
if lines[s[0]].startswith('%let filename='):
|
||||
# Check if this is in the images section
|
||||
if s[2] and 'images' in s[2]:
|
||||
images_sections.append(s)
|
||||
else:
|
||||
web_file_sections.append(s)
|
||||
|
||||
print(f'Service sections: {len(service_sections)}, Web file sections: {len(web_file_sections)}, Image sections: {len(images_sections)}')
|
||||
|
||||
# Service chunks (10 per chunk)
|
||||
chunk_size = 10
|
||||
for i in range(0, len(service_sections), chunk_size):
|
||||
chunk = service_sections[i:i + chunk_size]
|
||||
chunk_lines = header[:]
|
||||
for start, end, path in chunk:
|
||||
if path:
|
||||
chunk_lines.append(path)
|
||||
chunk_lines.extend(lines[start:end])
|
||||
cf = os.path.join(CHUNK_DIR, f'chunk_{len(chunk_files):03d}.sas')
|
||||
with open(cf, 'w') as f:
|
||||
f.writelines(chunk_lines)
|
||||
chunk_files.append(cf)
|
||||
print(f' {cf}: {len(chunk)} svcs, {len(chunk_lines)} lines, {sum(len(l) for l in chunk_lines) / 1024 / 1024:.1f} MB')
|
||||
|
||||
# Web file chunks (2 per chunk, 1 if large)
|
||||
web_chunk_size = 2
|
||||
for i in range(0, len(web_file_sections), web_chunk_size):
|
||||
chunk = web_file_sections[i:i + web_chunk_size]
|
||||
has_big = any(e - s > 20000 for s, e, _ in chunk)
|
||||
if has_big:
|
||||
for start, end, path in chunk:
|
||||
chunk_lines = header[:]
|
||||
chunk_lines.append(path or '%let path=services/web;\n\n')
|
||||
chunk_lines.extend(lines[start:end])
|
||||
cf = os.path.join(CHUNK_DIR, f'chunk_{len(chunk_files):03d}.sas')
|
||||
with open(cf, 'w') as f:
|
||||
f.writelines(chunk_lines)
|
||||
chunk_files.append(cf)
|
||||
print(f' {cf}: 1 web file (large), {len(chunk_lines)} lines, {sum(len(l) for l in chunk_lines) / 1024 / 1024:.1f} MB')
|
||||
else:
|
||||
chunk_lines = header[:]
|
||||
# Ensure %let path= is set
|
||||
path_set = False
|
||||
for start, end, path in chunk:
|
||||
if path and not path_set:
|
||||
chunk_lines.append(path)
|
||||
path_set = True
|
||||
elif path and path_set:
|
||||
# Different path for second file - include it inline
|
||||
chunk_lines.append(path)
|
||||
chunk_lines.extend(lines[start:end])
|
||||
cf = os.path.join(CHUNK_DIR, f'chunk_{len(chunk_files):03d}.sas')
|
||||
with open(cf, 'w') as f:
|
||||
f.writelines(chunk_lines)
|
||||
chunk_files.append(cf)
|
||||
print(f' {cf}: {len(chunk)} web files, {len(chunk_lines)} lines, {sum(len(l) for l in chunk_lines) / 1024 / 1024:.1f} MB')
|
||||
|
||||
# Images sections
|
||||
for start, end, path in images_sections:
|
||||
chunk_lines = header[:]
|
||||
if path:
|
||||
chunk_lines.append(path)
|
||||
chunk_lines.extend(lines[start:end])
|
||||
cf = os.path.join(CHUNK_DIR, f'chunk_{len(chunk_files):03d}.sas')
|
||||
with open(cf, 'w') as f:
|
||||
f.writelines(chunk_lines)
|
||||
chunk_files.append(cf)
|
||||
print(f' {cf}: images, {len(chunk_lines)} lines, {sum(len(l) for l in chunk_lines) / 1024 / 1024:.1f} MB')
|
||||
|
||||
print(f'\nDeploying {len(chunk_files)} chunks to nextviya...')
|
||||
|
||||
for idx, cf in enumerate(chunk_files, 1):
|
||||
print(f'\n[{idx}/{len(chunk_files)}] Deploying {os.path.basename(cf)} ...')
|
||||
timeout = 600 if os.path.getsize(cf) > 5 * 1024 * 1024 else 300
|
||||
result = subprocess.run(
|
||||
['npx', 'sasjs', 'run', cf, '-t', 'nextviya'],
|
||||
capture_output=True, text=True, timeout=timeout
|
||||
)
|
||||
out = '\n'.join([l for l in result.stdout.splitlines() if not l.startswith('isTokenExpiring')])
|
||||
tail_lines = out.splitlines()[-40:]
|
||||
print('\n'.join(tail_lines))
|
||||
if result.returncode != 0:
|
||||
print(f'FAILED with code {result.returncode}')
|
||||
print(result.stderr[-1000:] if len(result.stderr) > 1000 else result.stderr)
|
||||
sys.exit(1)
|
||||
print(f' -> OK')
|
||||
|
||||
print('\nAll chunks deployed successfully!')
|
||||
Reference in New Issue
Block a user