Search code examples
node.jstypescriptazure-web-app-serviceazure-pipelinesprisma

Node process is locking file in generated .prisma folder, preventing subsequent builds in an Azure Pipeline


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.


Solution

  • 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)