A DevSecOps pipeline like this is less about adding tools and more about changing control points. Once security checks become first-class jobs in GitHub Actions, the team has to treat failed scans as release blockers, not advisory output. That shifts responsibility from a manual review habit to an enforced delivery policy, which is only effective if the rules are consistent across pull requests, merges and production-bound releases.
The architecture also depends on job ordering and isolation. Running SAST, secrets detection and dependency auditing before the build reduces wasted compute and shortens feedback loops, but it also means the pipeline must be fast enough that developers do not bypass it in practice. For Node.js workloads, shallow clones, package-manager behavior and transitive dependencies all influence what gets inspected and when findings appear.
On Azure, deployment slots and GitHub Environments introduce a useful separation between verification and promotion. That separation matters operationally: staging can absorb smoke testing, while production requires an explicit approval gate and a controlled slot swap. The trade-off is added workflow complexity, but it is usually cheaper than recovering from a bad release or a leaked secret.
The strongest long-term value is in traceability. Linking workflow history with Azure Monitor creates an audit trail from commit to deployment, which helps incident response, change management and postmortems. The main maintenance burden is keeping the policy aligned with the codebase: tune severity thresholds, reduce false positives, rotate exposed secrets immediately and keep the pipeline lightweight enough that teams will actually use it.
Three years ago, two days before a production push to Azure, I was working through a pull request on a Node.js service. Routine stuff. Then I spotted something in a commit that had been merged two weeks earlier. Already past review, already on the main branch. A Stripe secret key. Not obfuscated, not in a config file. Plain text, right there in the source. Build was green. Tests passed. Nobody had caught it.
That incident changed how I think about CI/CD. The problem wasnโt that my team was careless. The problem was that our pipeline had no opinion about security. It checked code quality. It ran tests. It deployed. But it never asked whether what we were shipping was safe.
DevSecOps is the answer to that question, but the term gets thrown around loosely. At its core, it just means one thing: Security checks run automatically, in the pipeline, on every change, the same way unit tests do. If a security check fails, the build fails. No exceptions, no โweโll fix it in the next sprint.โ
This article builds exactly that kind of pipeline on Azure using GitHub Actions: Static analysis, secrets detection, dependency auditing, deployment gates and audit logging โ all wired into one workflow that runs on every PR and push to main. The YAML here is real; swap tool names to match your stack and it deploys.
The Pipeline at a Glance
Hereโs the full sequence before we dive into each piece:
- Trigger: PR opened or push to main
- Job 1: SAST scan (CodeQL) โ static analysis for code vulnerabilities
- Job 2: Secrets scan (Gitleaks) โ catch leaked credentials before they land in the repo
- Job 3: Dependency check โ npm audit or Snyk scanning third-party packages for CVEs
- Job 4: Build and test โ standard compilation, unit tests, integration tests
- Job 5: Deploy to staging slot โ push to Azure App Service staging, run smoke tests
- Job 6: Production gate โ required reviewer must approve before anything touches prod
- Job 7: Slot swap and audit log โ promote staging to production, write a deployment record to Azure Monitor
Running the first three jobs in parallel, before the build starts, isnโt arbitrary. Waiting on a slow build only to hit a security failure at the end is the worst possible timing. Usually, it hits when a release window is closing and the pressure to just push it anyway is at its highest. Security failures caught early get fixed. Security failures caught late get deferred.
SAST With CodeQL
SAST works on source code before anything is executed. No running server, no network call. It scans for coding patterns behind vulnerabilities such as SQL injection, XSS and insecure deserialization. These are genuinely hard to catch in a code review, especially when the team is under release pressure. GitHubโs CodeQL does this well, and itโs free for public repos.
Create .github/workflows/sast.yml:
name: SAST โ CodeQL
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
analyze:
runs-on: ubuntu-latest
permissions:
security-events: write
contents: read
steps:
- uses: actions/checkout@v4
- name: Initialize CodeQL
uses: github/codeql-action/init@v3
with:
languages: javascript
- name: Autobuild
uses: github/codeql-action/autobuild@v3
- name: Analyze
uses: github/codeql-action/analyze@v3
with:
category: '/language:javascript'
fail-on-severity: high
Results land in the Security tab under Code Scanning Alerts. The fail-on-severity: high setting means the pipeline blocks on critical and high findings but surfaces medium and low as warnings you can triage later. Thatโs a pragmatic starting point. You can tighten it over time.
One thing I found: CodeQLโs JavaScript queries catch a surprising number of prototype pollution bugs and unsafe eval() patterns that conventional linting misses. Itโs not a replacement for a manual security review, but it catches the mechanical stuff reliably.
Secrets Detection With Gitleaks
Hereโs a number that should worry you: According to GitGuardianโs 2024 report, a secret is exposed on GitHub every five seconds โ API keys, database URLs, JWT signing secrets, OAuth tokens. Developers commit them accidentally more than youโd think, often in a โquick fixโ commit made under pressure.
Gitleaks scans your commits for credential patterns before they reach the remote. Add it as a required check on every PR:
name: Secrets Scan โ Gitleaks
on:
pull_request:
branches: [main]
push:
branches: [main]
jobs:
gitleaks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Run Gitleaks
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: $
The fetch-depth: 0 is important. By default, actions/checkout does a shallow clone. Gitleaks needs the full commit history to scan past commits. A secret from six weeks ago in an old branch is still a secret.
Add a .gitleaks.toml at the repo root to handle false positives. Test fixtures that deliberately contain fake credentials are a common source of noise:
[allowlist]
paths = [
'''^test/fixtures/'''
'''^docs/examples/'''
]
One hard-learned lesson: If Gitleaks flags a real secret, deleting the commit isnโt enough. Rotate the credential immediately and assume it was already scraped. Bots index GitHub continuously, and thereโs no way to know how long the exposure window was.
Dependency Auditing
Open your package-lock.json and scroll for a moment. A typical Node.js project carries several hundred packages, most of which you never explicitly installed. Every package you depend on is a potential attack surface, and new CVEs are disclosed every week. Without automated auditing, you wonโt know about them until someone finds them in your app.
npm audit covers the basics. Snyk adds richer CVE data and tracks transitive vulnerabilities more thoroughly. Hereโs a job that runs both:
name: Dependency Audit
on:
pull_request:
branches: [main]
push:
branches: [main]
schedule:
- cron: '0 6 * * 1' # weekly Monday scan
jobs:
audit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
- run: npm ci
- name: npm audit
run: npm audit --audit-level=high
- name: Snyk scan
uses: snyk/actions/node@master
env:
SNYK_TOKEN: $
with:
args: --severity-threshold=high
The schedule trigger is the part most teams skip. It catches CVEs disclosed after your last deployment. A package you installed three months ago might have a critical vulnerability published yesterday. The weekly scan makes sure you donโt find out about it from a penetration tester.
Azure Deployment Gates
Code that passes every automated check still needs a human decision before it goes to production. Not for every deployment, that would kill velocity, but for production-bound changes, a required reviewer is your last line of defense.
GitHub Environments handle this cleanly. Go to your repo Settings > Environments > New environment, name it โproductionโ and add required reviewers. Any job targeting that environment pauses until someone on the list approves.
Pair that with Azure App Service deployment slots and you get a safe promotion workflow:
jobs:
deploy-staging:
runs-on: ubuntu-latest
steps:
- uses: azure/login@v1
with:
creds: $
- name: Deploy to staging slot
uses: azure/webapps-deploy@v3
with:
app-name: my-app
slot-name: staging
package: ./dist
- name: Smoke tests
The slot swap is near-instant and zero-downtime. If something breaks in production after the swap, you can reverse it in the Azure portal in about 30 seconds โ no redeploy, no rollback drama. After an incident, two questions come up immediately: What changed and who did it? GitHub Actions answers this for free through its workflow run history. Centralizing that data in Azure Monitor gives you a single place to query across your entire infrastructure โ not just GitHub. Add this to your deploy job: This creates a timestamped record in Azure Monitor for every production deployment: Who triggered it, from which commit and which branch. Pair it with Application Insights for runtime telemetry and you have end-to-end traceability โ from code commit to live behavior. A few patterns that consistently undermine DevSecOps pipelines, based on implementations Iโve been part of or reviewed: The pipeline described here covers the fundamentals. Done consistently, they prevent most incidents. No SIEM budget required, no AppSec hire. What actually changes the outcome is automated gates that run on every change and stop the build when something fails. Start with CodeQL and Gitleaks. Theyโre free, they integrate cleanly with GitHub Actions and theyโll catch real problems within the first week. Add Snyk once you have a handle on your dependency surface. Deployment gates and audit logging can come in parallel โ neither requires significant setup on Azure. The shift-left principle isnโt complicated: Fix security problems when theyโre cheap to fix, which is before the code is deployed. A pipeline that enforces this automatically is what makes that principle real. run: npm run test:smoke -- --url Original Post>
promote-to-production: needs: deploy-staging runs-on: ubuntu-latest environment: production # triggers reviewer gate steps: - uses: azure/login@v1 with: creds: $ - name: Swap staging to production uses: azure/cli@v1 with: inlineScript: | az webapp deployment slot swap \ --name my-app \ --resource-group my-rg \ --slot staging \ --target-slot productionAudit Logging
- name: Write deployment record uses: azure/cli@v1 with: inlineScript: | az monitor activity-log alert create \ --name "deploy-$" \ --resource-group my-rg \ --description "Actor: $ | SHA: $ | Ref: $"Mistakes I See Teams Make
Where to Go From Here
How to Build a DevSecOps CI/CD Pipeline on Azure With GitHub Actions
Enjoyed this article? Sign up for our newsletter to receive regular insights and stay connected.

