Node: 20.11.1 Azure Pipeline: Ubuntu 22.04.4 LTS Prisma: 5.10.2
I have an Azure pipeline that is running an ExpressJS app written in typescript.
Here is my package.json:
{
"name": "...",
"version": "0.1.0",
"description": "...",
"main": "js/index.js",
"scripts": {
"watch": "npx tsc -w --preserveWatchOutput",
"dev": "tsx watch index.ts --no-cache",
"clean": "rm -rf ./js/*",
"migrate": "npx prisma migrate deploy",
"prisma-client": "npx prisma generate",
"build": "tsc --build",
"start": "node js/index.js",
"serve": "npm run build;npm run start"
},
"author": "",
"license": "ISC",
"type": "module",
"dependencies": {
"@prisma/client": "^5.10.2",
"@types/connect-pg-simple": "^7.0.3",
"@types/express-session": "^1.17.7",
"@types/node-cron": "^3.0.11",
"@types/simple-oauth2": "^5.0.4",
"algoliasearch": "^4.20.0",
"connect-pg-simple": "^9.0.1",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"express-session": "^1.17.3",
"isomorphic-dompurify": "^2.3.0",
"node-cron": "^3.0.3",
"node-fetch": "^3.3.2",
"simple-oauth2": "^5.0.0"
},
"devDependencies": {
"@types/express": "^4.17.17",
"@types/node": "^20.2.5",
"prisma": "^5.10.2",
"ts-node": "^10.9.2",
"tsx": "^4.0.0",
"typescript": "^5.0.4"
},
"engines": {
"npm": ">=9.6.0",
"node": ">=18.17.0"
}
}
Here is my pipeline YAML:
trigger:
- master
variables:
azureSubscription: "..."
webAppName: "..."
resourceGroupName: "..."
slotName: "..."
environmentName: "..."
vmImageName: "ubuntu-latest"
stages:
- stage: Build
displayName: Build stage
jobs:
- job: Build
displayName: Build
pool:
vmImage: $(vmImageName)
steps:
- task: NodeTool@0
inputs:
versionSpec: "18.x"
displayName: "Install Node.js"
- script: |
npm install
npm run build --if-present
npm run test --if-present
npm run prisma-client --if-present
displayName: "npm install, build, test & generate prisma client"
- task: ArchiveFiles@2
displayName: "Archive files"
inputs:
rootFolderOrFile: "$(System.DefaultWorkingDirectory)"
includeRootFolder: false
archiveType: zip
archiveFile: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
replaceExistingArchive: true
- upload: $(Build.ArtifactStagingDirectory)/$(Build.BuildId).zip
artifact: drop
- stage: Deploy
displayName: Deploy stage
dependsOn: Build
condition: succeeded()
jobs:
- deployment: Deploy
displayName: Deploy
environment: $(environmentName)
pool:
vmImage: $(vmImageName)
strategy:
runOnce:
deploy:
steps:
- task: AzureWebApp@1
displayName: "Azure Web App Deploy"
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
deployToSlotOrASE: true
slotName: $(slotName)
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
startUpCommand: "npm run serve"
The Azure Web App has the following post deployment actions commands in the file /home/site/wwwroot/deployments/tools/PostDeploymentActions/actions.cmd
:
echo "Running actions.cmd post deployment script."
echo "Changing directory to /home/site/wwwroot"
cd /home/site/wwwroot
echo "Publish prisma schema."
npm run prisma-client
echo "Execute database migrations."
npm run migrate
echo "Post deployment actions completed."
For a fresh deployment, with the wwwroot folder empty, the deploy succeeds and npm run prisma-client
runs successfully as part of the post deployment actions.
Then the startUpCommand
command runs and the app runs as expected.
However, on the second deployment npm run prisma-client
fails with the following error:
Error:
ENOENT: no such file or directory, copyfile '/home/site/wwwroot/node_modules/prisma/libquery_engine-debian-openssl-3.0.x.so.node' -> '/home/site/wwwroot/node_modules/.prisma/client/libquery_engine-debian-openssl-3.0.x.so.node'
Full error:
Command: "/home/site/deployments/tools/PostDeploymentActions/actions.cmd"
Running actions.cmd post deployment script.
Changing directory to /home/site/wwwroot
Publish prisma schema.
> afs-middleware@0.1.0 prisma-client
> npx prisma generate
Prisma schema loaded from prisma/schema.prisma
Error:
ENOENT: no such file or directory, copyfile '/home/site/wwwroot/node_modules/prisma/libquery_engine-debian-openssl-3.0.x.so.node' -> '/home/site/wwwroot/node_modules/.prisma/client/libquery_engine-debian-openssl-3.0.x.so.node'
Execute database migrations.
> afs-middleware@0.1.0 migrate
> npx prisma migrate deploy
Prisma schema loaded from prisma/schema.prisma
Datasource "db": PostgreSQL database "...", schema "public" at "..."
44 migrations found in prisma/migrations
No pending migrations to apply.
Post deployment actions completed.
When SSH'ing into the server, I run the following:
fuser --verbose /home/site/wwwroot/node_modules/.prisma/client/libquery_engine-debian-openssl-3.0.x.so.node
This shows the PID of a process that is using this file.
root 168 ....m node
If I run ps -aux
I can see that the process is root 168 0.2 2.5 11754208 82204 ? SNl 03:53 0:02 node js/index.js
, the startUpCommand
of the server.
If I manually kill this process over SSH, then the next deploy works.
for file in node_modules/.prisma/client/*; do kill -9 $(fuser --verbose "$file" 2>/dev/null | awk '{print $1}'); done
.
I've tried to automate this, adding the above command to /home/site/wwwroot/deployments/tools/PostDeploymentActions/actions.cmd
, but it seems it doesn't have permission to see the process.
I ended up being able to fix the issue if I just stopped and restarted my app service as part of my deploy pipeline. https://learn.microsoft.com/en-us/azure/devops/pipelines/tasks/reference/azure-app-service-manage-v0?view=azure-pipelines
- task: AzureAppServiceManage@0
displayName: "Stop Web App"
inputs:
azureSubscription: $(azureSubscription)
Action: "Stop Azure App Service"
WebAppName: $(webAppName)
SpecifySlotOrASE: true
ResourceGroupName: $(resourceGroupName)
Slot: $(slotName)
- task: AzureWebApp@1
displayName: "Azure Web App Deploy"
inputs:
azureSubscription: $(azureSubscription)
appType: webAppLinux
appName: $(webAppName)
deployToSlotOrASE: true
slotName: $(slotName)
package: $(Pipeline.Workspace)/drop/$(Build.BuildId).zip
- task: AzureAppServiceManage@0
displayName: "Start Web App"
inputs:
azureSubscription: $(azureSubscription)
Action: "Start Azure App Service"
WebAppName: $(webAppName)
SpecifySlotOrASE: true
ResourceGroupName: $(resourceGroupName)
Slot: $(slotName)