DevOps · Devops

Acumatica CI/CD with GitHub Actions

How to build a CI/CD pipeline for Acumatica customizations with GitHub Actions — build, test, package, publish to staging, run smoke tests, and gate production deploys.

John Kihiu12 min read

Acumatica does not ship a CI/CD product, but a customization project is just a zip of DLLs, ASPX pages, and metadata that the platform can import through its API — which means every piece of a real pipeline (build, package, deploy, smoke test) is scriptable. GitHub Actions is a reasonable home for that pipeline if your source lives on GitHub: no infrastructure to run, and the runner can build the customization project and push it to a target instance over HTTP.

What the pipeline actually builds

An Acumatica customization is typically developed as a Visual Studio class library (for code extensions — PXGraphExtension, DAC extensions) plus loose ASPX/ASCX pages for UI, packaged into a .zip customization project. The build step compiles the class library against the Acumatica SDK assemblies (referenced as NuGet packages or a local SDK folder), and the package step assembles the zip in the layout Acumatica's import expects — a ClassFiles folder for compiled DLLs, a Files folder for pages, and the manifest.

YAML · .GITHUB/WORKFLOWS/DEPLOY.YML
name: Build and Deploy Acumatica Customization
on:
  push:
    branches: [main]

jobs:
  build:
    runs-on: windows-latest
    steps:
      - uses: actions/checkout@v4
      - name: Setup MSBuild
        uses: microsoft/setup-msbuild@v2
      - name: Restore and build extension library
        run: |
          nuget restore CustomizationExt.sln
          msbuild CustomizationExt.sln /p:Configuration=Release
      - name: Package customization zip
        run: powershell ./scripts/package-customization.ps1
      - uses: actions/upload-artifact@v4
        with:
          name: customization-package
          path: dist/*.zip

  deploy-staging:
    needs: build
    runs-on: windows-latest
    steps:
      - uses: actions/download-artifact@v4
        with: { name: customization-package }
      - name: Publish to staging via Acumatica REST/import API
        env:
          ACUMATICA_URL: ${{ secrets.STAGING_URL }}
          ACUMATICA_USER: ${{ secrets.STAGING_USER }}
          ACUMATICA_PASS: ${{ secrets.STAGING_PASS }}
        run: powershell ./scripts/publish-customization.ps1

Why MSBuild and not a container

Acumatica's SDK assemblies and the class library extension model target .NET Framework in most current versions, which is why the workflow above runs on windows-latest rather than a Linux container — MSBuild against the Acumatica assemblies needs the full .NET Framework toolchain that ships with the Windows runner image. This is the single biggest surprise for teams used to Linux-first CI: you cannot lift-and-shift a typical dotnet-core pipeline here without checking which Acumatica version and edition you are targeting.

Credentials never belong in the workflow file

The publish step authenticates against a real Acumatica instance. Store the URL, user, and password (or better, an API-only integration user with a scoped role) as GitHub Actions repository or environment secrets, and gate the production deploy job behind a required reviewer using GitHub Environments — a customization publish is not something you want firing unattended on every merge to main.

Publishing through the API, not the screen

Acumatica exposes customization project import/publish through its contract-based REST API and through a SOAP-based screen-based API used historically for the Customization Projects (SM204505) screen. Either can be scripted from PowerShell: log in, POST the package to the import endpoint, then trigger a publish. Because publishing recompiles the site and can take several minutes, the workflow should poll for completion rather than assume the call is synchronous, and should capture the publish log so a failure surfaces in the Actions run instead of silently leaving staging on the old build.

Smoke tests after publish

A publish that "succeeds" can still leave the site broken if a screen fails to compile cleanly. After the publish step, run a lightweight smoke test — log in via the REST API, open two or three screens that exercise the customization, and check for a 200 response and absence of the generic error page markup. This does not replace real regression testing, but it catches the class of failure ("the whole site is down") that would otherwise wait for a human to notice.

Staging first, production gated

Keep production as a separate job that only runs after staging succeeds and, ideally, after a manual approval step. GitHub Environments support required reviewers natively, so the same workflow file can enforce "someone looked at staging" before the production publish job is allowed to run — no separate approval tooling needed.

Wrapping up

The mechanics are unusual compared to a typical web app pipeline — Windows runners, MSBuild, a proprietary publish API — but the shape is the same CI/CD discipline: build once, package once, deploy the same artifact through increasingly gated environments, and smoke-test after every publish. Once the scripts exist they are reusable across every customization project you maintain, which is where the investment pays off.

John Kihiu
Acumatica ERP Developer · Laravel Engineer

Independent software engineer in Nairobi specialising in Acumatica customisations, Laravel backends, and tax fiscalisation integrations across East and Southern Africa.