fix(deploy): Viya deploy checks, startup diagnostics, and chunked deploy script
Build / Build-and-ng-test (pull_request) Failing after 2m43s
Build / Build-and-test-development (pull_request) Skipped
Lighthouse Checks / lighthouse (pull_request) Successful in 21m6s

- 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:
hermes
2026-08-21 16:00:59 +01:00
parent 12091c4044
commit 917925f553
14 changed files with 537 additions and 101 deletions
+39
View File
@@ -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 -->
+10
View File
@@ -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')
+19 -1
View File
@@ -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 ||
+5 -2
View File
@@ -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
}
+77 -11
View File
@@ -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
View File
@@ -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;
}
+70 -70
View File
@@ -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": {
+11 -11
View File
@@ -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
View File
@@ -30,7 +30,7 @@
},
"private": true,
"dependencies": {
"@sasjs/cli": "4.19.0",
"@sasjs/cli": "4.20.1",
"@sasjs/core": "5.1.0"
},
"overrides": {
+155
View File
@@ -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!')