Skip to content
This repository was archived by the owner on May 12, 2026. It is now read-only.

Ci/v2 pipeline - #18

Merged
BAGHIRA-0F-RELIGIONS merged 6 commits into
hngprojects:devfrom
B-Nockk:ci/v2-pipeline
May 10, 2026
Merged

Ci/v2 pipeline#18
BAGHIRA-0F-RELIGIONS merged 6 commits into
hngprojects:devfrom
B-Nockk:ci/v2-pipeline

Conversation

@B-Nockk

@B-Nockk B-Nockk commented May 10, 2026

Copy link
Copy Markdown
Contributor

Description

Set up a modular GitHub Actions CI/CD pipeline. Instead of one giant file, I’ve broken the logic into reusable building blocks (nested workflows) for better maintenance and readability.

Related Issue

Fixes # (issue)

Type of Change

  • feat: New feature (CI/CD Pipeline)
  • chore: Build process or tooling changes

Summary of Work

  • Modular Design: Created sub-workflows for lint, test, security, build, and deploy.

  • Security: Added Gitleaks for secret scanning and npm audit for dependency checks.

  • Smart Triggers:

  • PRs: Only run Lint & Build (safely validates code).

  • Pushes: Automatic deployment to dev, staging, or prod based on the branch name.

  • Manual: Added workflow_dispatch to trigger deploys manually from the Actions tab.

  • Deployment: Uses sshpass and scp to bundle the app, ship it to the server, and restart via PM2.

How Has This Been Tested?

  • Manual tests (Triggered via workflow_dispatch)
  • Integration tests (Ran full pipeline on dev branch)

Checklist

  • My code follows the project's coding style
  • My changes generate no new warnings
  • New and existing unit tests pass locally with my changes

Additional Notes

Tests are currently commented out in the main pipeline until the team has written actual test files—uncomment test-pr or test when ready!

Summary by CodeRabbit

  • Chores
    • Restructured continuous integration and deployment pipeline with modular, reusable workflows.
    • Consolidated separate deployment workflows into a unified automated pipeline.
    • Enhanced quality gates with automated security audits and code linting.
    • Standardized deployment process across development, staging, and production environments.

Review Change Stack

@gemini-code-assist

Copy link
Copy Markdown

Note

Gemini is unable to generate a review for this pull request due to the file types involved not being currently supported.

@coderabbitai

coderabbitai Bot commented May 10, 2026

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

This PR refactors the CI/CD pipeline from three separate environment-specific deployment workflows (dev-deployment.yaml, staging-deployment.yaml, main-deployment.yaml) plus a pull-request workflow (lint-build-test.yaml) into a reusable, modular architecture. It introduces five reusable workflows (_build.yml, _deploy.yml, _lint.yml, _security.yml, _test.yml) and a main orchestrator (pipeline.yml) that coordinates validation, building, and multi-environment deployments based on branch and manual trigger events.

Changes

CI/CD Pipeline Modularization

Layer / File(s) Summary
Validation & Gate Workflows
.github/workflows/_lint.yml, .github/workflows/_security.yml, .github/workflows/_test.yml
Three reusable workflows for linting, testing (with workflow_call and workflow_dispatch triggers), and security (npm audit and Gitleaks secret scanning). Each sets up Node.js and runs validation commands; security workflow runs two parallel jobs.
Build & Deploy Workflows
.github/workflows/_build.yml, .github/workflows/_deploy.yml
Reusable build workflow that accepts environment input, compiles code, packages runtime files and ecosystem config into a tarball, exports metadata (tarball, artifact_name) via $GITHUB_OUTPUT, and uploads as 1-day ephemeral artifact. Reusable deploy workflow that downloads the artifact, transfers the tarball via sshpass/scp, SSHes into the remote server, extracts the tarball, moves runtime files, and executes deployment.sh with PM2 environment (mapping prodmain, others unchanged).
Pipeline Orchestration
.github/workflows/pipeline.yml
Main workflow triggered on push/PR to dev, staging, main branches, plus workflow_dispatch with environment selector. Defines PR-only validation jobs (lint-pr, build-pr with fixed environment: dev), non-PR gates (lint, security via reusable workflows), environment-mapped build job (routing mainprod, stagingstaging, others → dev, plus inputs.environment from dispatch), and three conditional deploy jobs (deploy-dev, deploy-staging, deploy-prod) that guard execution on branch/dispatch match and consume build outputs.
Legacy Workflow Migration
.github/workflows/dev-deployment.yaml, .github/workflows/lint-build-test.yaml, .github/workflows/main-deployment.yaml, .github/workflows/staging-deployment.yaml
Removed four individual workflows that previously handled PR validation and environment-specific deployments; their logic is now consolidated into the modular reusable workflows and main pipeline.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Ci/v2 pipeline' is directly related to the main changeset, which introduces a new modular GitHub Actions CI/CD pipeline architecture (v2) replacing the previous monolithic workflow approach.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 8

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/_build.yml:
- Around line 3-10: The workflow_dispatch trigger is missing the environment
input used later as inputs.environment; add an environment input under
workflow_dispatch with the same config as workflow_call (description: 'Target
environment — controls which ecosystem config is bundled (dev | staging |
prod)', required: true, type: string) so that the manual trigger provides a
defined inputs.environment value referenced by the build steps; update the
top-level workflow_dispatch block (the workflow trigger section) to include this
input so artifact/file names that use inputs.environment are valid.

In @.github/workflows/_deploy.yml:
- Around line 43-52: Replace the password-based sshpass setup with SSH key-based
auth: remove the "Install sshpass" and "Prepare SSH helper" steps that export
SSHPASS, add an actions/ssh-agent step to load a private key from a secret
(e.g., PRIV_KEY) into the agent, and ensure the subsequent SSH commands (the
remote mkdir, scp, and ssh execution steps) run without sshpass so they
authenticate via the agent/private key; also update documentation or deployment
servers to have the corresponding public key installed in authorized_keys.
- Around line 55-57: Remove the StrictHostKeyChecking=no flags from all SSH and
SCP invocations (the sshpass -e ssh "... mkdir -p /home/${{ secrets.USERNAME
}}/tmp/nestjs" command and the other two remote commands/scp lines) and add a
preceding workflow step that populates ~/.ssh/known_hosts with the target host
key using the secrets.HOST value (e.g., run ssh-keyscan -H ${{ secrets.HOST }}
>> ~/.ssh/known_hosts or equivalent) so host key verification is enforced for
subsequent ssh/scp actions; ensure the new step runs before any sshpass/scp
steps and that secrets.USERNAME and secrets.HOST are still used for remote
operations.
- Around line 55-57: The ssh/scp invocations (e.g., the ssh call using "sshpass
-e ssh" that runs "mkdir -p /home/${{ secrets.USERNAME }}/tmp/nestjs" and the
other two scp/ssh calls) lack timeouts and can hang; update each invocation to
include SSH connection/session timeouts and a bounded overall command timeout:
add SSH options like -o ConnectTimeout=10 -o ConnectionAttempts=2 -o
ServerAliveInterval=15 -o ServerAliveCountMax=2 to the ssh/scp commands, and
wrap the whole invocation with a timeout (e.g., prefix with "timeout 60s") so
the pipeline fails fast on network issues; apply these changes to every
occurrence of "sshpass -e ssh" and "scp -o ..." in the workflow.

In @.github/workflows/_security.yml:
- Around line 3-7: Add an explicit least-privilege permissions block to the
reusable workflow by declaring permissions: contents: read at the top-level of
the workflow (alongside on:), so both jobs (audit and secret-scan) run with
read-only repo access; update the workflow file to include this permissions
declaration to enforce least privilege for the reusable workflow.
- Around line 24-25: The CI step named "Run npm audit" currently invokes "npm
audit --audit-level=high --omit=dev" which skips auditing dev dependencies even
though the workflow installs dev deps; update that step to run a full audit by
removing the "--omit=dev" flag (i.e., run "npm audit --audit-level=high") so the
audit covers the entire dependency tree including dev dependencies and CI-only
tools used in the build.

In @.github/workflows/_test.yml:
- Around line 12-13: Add least-privilege workflow permissions and prevent
checkout from persisting GitHub credentials: at the top-level of the workflow
add a permissions block with contents: read (i.e., permissions: contents: read)
and update the actions/checkout@v4 step (the "Checkout code" step) to include
persist-credentials: false so the runner won't retain the GITHUB_TOKEN for
subsequent commands (reducing token exposure during npm install or other
scripts).

In @.github/workflows/pipeline.yml:
- Around line 115-119: The workflow's manual trigger condition allows
workflow_dispatch from any branch because it only checks inputs.environment;
update the if condition for the deploy jobs (e.g., the block referencing
github.event_name, github.ref, inputs.environment and the deploy-dev job) to
require both inputs.environment == 'dev' AND github.ref == 'refs/heads/dev' for
workflow_dispatch paths (similarly enforce the correct branch for other
environments), so manual deployments only run when the trigger comes from the
matching branch as well as the matching environment input.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: e1648e70-92a1-4fb5-abbd-6c5f8f5b1715

📥 Commits

Reviewing files that changed from the base of the PR and between b81f664 and 32a5c16.

📒 Files selected for processing (10)
  • .github/workflows/_build.yml
  • .github/workflows/_deploy.yml
  • .github/workflows/_lint.yml
  • .github/workflows/_security.yml
  • .github/workflows/_test.yml
  • .github/workflows/dev-deployment.yaml
  • .github/workflows/lint-build-test.yaml
  • .github/workflows/main-deployment.yaml
  • .github/workflows/pipeline.yml
  • .github/workflows/staging-deployment.yaml
💤 Files with no reviewable changes (4)
  • .github/workflows/staging-deployment.yaml
  • .github/workflows/lint-build-test.yaml
  • .github/workflows/main-deployment.yaml
  • .github/workflows/dev-deployment.yaml

Comment on lines +3 to +10
on:
workflow_dispatch:
workflow_call:
inputs:
environment:
description: 'Target environment — controls which ecosystem config is bundled (dev | staging | prod)'
required: true
type: string

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/_build.yml

Repository: hngprojects/flowbrand-be

Length of output: 2539


workflow_dispatch currently has no environment input, but the workflow requires it.

Lines 20–22 define environment variables using inputs.environment; on manual runs via workflow_dispatch this value is undefined, resulting in invalid filenames and artifact names (e.g., nestjs-.tar.gz, build-), causing failures in lines 49, 52, and 63. Add the environment input to workflow_dispatch with the same configuration as workflow_call.

Suggested change
 on:
   workflow_dispatch:
+    inputs:
+      environment:
+        description: Target environment
+        required: true
+        type: choice
+        options: [dev, staging, prod]
   workflow_call:
     inputs:
       environment:
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/_build.yml around lines 3 - 10, The workflow_dispatch
trigger is missing the environment input used later as inputs.environment; add
an environment input under workflow_dispatch with the same config as
workflow_call (description: 'Target environment — controls which ecosystem
config is bundled (dev | staging | prod)', required: true, type: string) so that
the manual trigger provides a defined inputs.environment value referenced by the
build steps; update the top-level workflow_dispatch block (the workflow trigger
section) to include this input so artifact/file names that use
inputs.environment are valid.

Comment on lines +43 to +52
- name: Install sshpass
run: sudo apt-get install -y sshpass

- name: Prepare SSH helper
# Write a one-liner wrapper so we don't repeat the sshpass boilerplate.
# SSHPASS env var is read by sshpass automatically — keeps the password
# out of the process argument list.
run: |
echo "SSHPASS=${{ secrets.PASSWORD }}" >> $GITHUB_ENV

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major | 🏗️ Heavy lift

❓ Verification inconclusive

Script executed:

cat -n .github/workflows/_deploy.yml

Repository: hngprojects/flowbrand-be


Repository: hngprojects/flowbrand-be
Exit code: 0

stdout:

     1	name: _deploy
     2	
     3	on:
     4	  workflow_call:
     5	    inputs:
     6	      environment:
     7	        description: 'Target environment (dev | staging | prod)'
     8	        required: true
     9	        type: string
    10	      artifact_name:
    11	        description: 'GitHub Actions artifact name produced by the build job'
    12	        required: true
    13	        type: string
    14	      tarball:
    15	        description: 'Tarball filename (e.g. nestjs-dev.tar.gz)'
    16	        required: true
    17	        type: string
    18	    secrets:
    19	      HOST:
    20	        required: true
    21	      USERNAME:
    22	        required: true
    23	      PASSWORD:
    24	        required: true
    25	
    26	env:
    27	  DEPLOY_DIR: ${{ vars.DEPLOY_DIR }}
    28	  PM2_ENV: ${{ inputs.environment == 'prod' && 'main' || inputs.environment }}
    29	  REMOTE_TMP: /home/${{ secrets.USERNAME }}/tmp/nestjs
    30	
    31	jobs:
    32	  deploy:
    33	    name: Deploy (${{ inputs.environment }})
    34	    runs-on: ubuntu-latest
    35	    environment: ${{ inputs.environment }}
    36	
    37	    steps:
    38	      - name: Download build artifact
    39	        uses: actions/download-artifact@v4
    40	        with:
    41	          name: ${{ inputs.artifact_name }}
    42	
    43	      - name: Install sshpass
    44	        run: sudo apt-get install -y sshpass
    45	
    46	      - name: Prepare SSH helper
    47	        # Write a one-liner wrapper so we don't repeat the sshpass boilerplate.
    48	        # SSHPASS env var is read by sshpass automatically — keeps the password
    49	        # out of the process argument list.
    50	        run: |
    51	          echo "SSHPASS=${{ secrets.PASSWORD }}" >> $GITHUB_ENV
    52	
    53	      - name: Ensure remote tmp directory exists
    54	        run: |
    55	          sshpass -e ssh -o StrictHostKeyChecking=no \
    56	            ${{ secrets.USERNAME }}@${{ secrets.HOST }} \
    57	            "mkdir -p /home/${{ secrets.USERNAME }}/tmp/nestjs"
    58	
    59	      - name: Copy tarball to server
    60	        run: |
    61	          echo "══════════════════════════════════════════"
    62	          echo "  Copying ${{ inputs.tarball }} → ${{ secrets.HOST }}:${{ env.REMOTE_TMP }}"
    63	          echo "══════════════════════════════════════════"
    64	          sshpass -e scp -o StrictHostKeyChecking=no \
    65	            ${{ inputs.tarball }} \
    66	            ${{ secrets.USERNAME }}@${{ secrets.HOST }}:${{ env.REMOTE_TMP }}/
    67	
    68	      - name: Deploy on server
    69	        env:
    70	          TARBALL: ${{ inputs.tarball }}
    71	          ENVIRONMENT: ${{ inputs.environment }}
    72	        run: |
    73	          sshpass -e ssh -o StrictHostKeyChecking=no \
    74	            ${{ secrets.USERNAME }}@${{ secrets.HOST }} << REMOTE
    75	            set -euo pipefail
    76	
    77	            DEPLOY_DIR="${{ env.DEPLOY_DIR }}"
    78	            PM2_ENV="${{ env.PM2_ENV }}"
    79	            REMOTE_TMP="/home/${{ secrets.USERNAME }}/tmp/nestjs"
    80	            TARBALL="${{ inputs.tarball }}"
    81	
    82	            echo "══════════════════════════════════════════"
    83	            echo "  Environment : ${{ inputs.environment }}"
    84	            echo "  Deploy dir  : \${DEPLOY_DIR}"
    85	            echo "  PM2 env     : \${PM2_ENV}"
    86	            echo "══════════════════════════════════════════"
    87	
    88	            echo "── Navigating to deploy dir ───────────────"
    89	            cd "\${DEPLOY_DIR}"
    90	
    91	            echo "── Extracting tarball ─────────────────────"
    92	            tar -xzf "\${REMOTE_TMP}/\${TARBALL}" -C .
    93	            rm -f "\${REMOTE_TMP}/\${TARBALL}"
    94	
    95	            echo "── Moving runtime files out of dist ───────"
    96	            mv dist/package.json .
    97	            mv dist/*-ecosystem-config.json .
    98	            mv dist/deployment.sh .
    99	
   100	            echo "── Running deployment script ──────────────"
   101	            chmod +x deployment.sh
   102	            ./deployment.sh "\${PM2_ENV}"
   103	
   104	            echo "══════════════════════════════════════════"
   105	            echo "  ✅ Deploy complete (${{ inputs.environment }})"
   106	            echo "══════════════════════════════════════════"
   107	          REMOTE


Replace password authentication with SSH key-based authentication.

The current password-based approach with sshpass is less secure and harder to rotate. SSH keys are the standard for automated deployments in CI/CD—GitHub Actions natively supports them via secrets and ssh-agent. This would require provisioning the public key on your deployment servers and updating the three SSH commands (mkdir, scp, and ssh) to use key-based auth, but the security improvement justifies the effort.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/_deploy.yml around lines 43 - 52, Replace the
password-based sshpass setup with SSH key-based auth: remove the "Install
sshpass" and "Prepare SSH helper" steps that export SSHPASS, add an
actions/ssh-agent step to load a private key from a secret (e.g., PRIV_KEY) into
the agent, and ensure the subsequent SSH commands (the remote mkdir, scp, and
ssh execution steps) run without sshpass so they authenticate via the
agent/private key; also update documentation or deployment servers to have the
corresponding public key installed in authorized_keys.

Comment on lines +55 to +57
sshpass -e ssh -o StrictHostKeyChecking=no \
${{ secrets.USERNAME }}@${{ secrets.HOST }} \
"mkdir -p /home/${{ secrets.USERNAME }}/tmp/nestjs"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cd /dev/null && git -C . ls-files ".github/workflows/_deploy.yml" 2>/dev/null || find . -name "_deploy.yml" -type f 2>/dev/null | head -5

Repository: hngprojects/flowbrand-be

Length of output: 168


🏁 Script executed:

cat -n .github/workflows/_deploy.yml 2>/dev/null || echo "File not found in expected location"

Repository: hngprojects/flowbrand-be

Length of output: 4493


Remove StrictHostKeyChecking=no and populate SSH known_hosts to prevent MITM attacks.

SSH host key verification is disabled across all remote operations (lines 55, 64, 73), creating a man-in-the-middle vulnerability. This allows attackers to intercept both artifact transfer (scp) and remote command execution (ssh), which is a release-blocking security risk.

Add a step to populate ~/.ssh/known_hosts with the deployment host's key before SSH operations, then remove the StrictHostKeyChecking=no flag from all three commands:

Suggested change
+      - name: Trust deploy host key
+        run: |
+          mkdir -p ~/.ssh
+          ssh-keyscan -H "${{ secrets.HOST }}" >> ~/.ssh/known_hosts
+
       - name: Ensure remote tmp directory exists
         run: |
-          sshpass -e ssh -o StrictHostKeyChecking=no \
+          sshpass -e ssh \
             ${{ secrets.USERNAME }}@${{ secrets.HOST }} \
             "mkdir -p /home/${{ secrets.USERNAME }}/tmp/nestjs"

       - name: Copy tarball to server
         run: |
           echo "══════════════════════════════════════════"
           echo "  Copying ${{ inputs.tarball }} → ${{ secrets.HOST }}:${{ env.REMOTE_TMP }}"
           echo "══════════════════════════════════════════"
-          sshpass -e scp -o StrictHostKeyChecking=no \
+          sshpass -e scp \
             ${{ inputs.tarball }} \
             ${{ secrets.USERNAME }}@${{ secrets.HOST }}:${{ env.REMOTE_TMP }}/

       - name: Deploy on server
         env:
           TARBALL: ${{ inputs.tarball }}
           ENVIRONMENT: ${{ inputs.environment }}
         run: |
-          sshpass -e ssh -o StrictHostKeyChecking=no \
+          sshpass -e ssh \
             ${{ secrets.USERNAME }}@${{ secrets.HOST }} << REMOTE
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/_deploy.yml around lines 55 - 57, Remove the
StrictHostKeyChecking=no flags from all SSH and SCP invocations (the sshpass -e
ssh "... mkdir -p /home/${{ secrets.USERNAME }}/tmp/nestjs" command and the
other two remote commands/scp lines) and add a preceding workflow step that
populates ~/.ssh/known_hosts with the target host key using the secrets.HOST
value (e.g., run ssh-keyscan -H ${{ secrets.HOST }} >> ~/.ssh/known_hosts or
equivalent) so host key verification is enforced for subsequent ssh/scp actions;
ensure the new step runs before any sshpass/scp steps and that secrets.USERNAME
and secrets.HOST are still used for remote operations.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/_deploy.yml

Repository: hngprojects/flowbrand-be

Length of output: 4493


🌐 Web query:

SSH ConnectTimeout ServerAliveInterval ServerAliveCountMax sshpass timeout options

💡 Result:

The query refers to SSH and sshpass timeout-related options: ConnectTimeout, ServerAliveInterval, ServerAliveCountMax, and sshpass timeout options. OpenSSH ssh client options (from ssh_config(5) and ssh(1) man pages): - ConnectTimeout: Specifies the timeout (in seconds) for connecting to the SSH server, covering TCP connection and initial handshake. Use on command line as ssh -o ConnectTimeout=10 user@host or in ~/.ssh/config [1][2][3][4]. - ServerAliveInterval: Interval (seconds) after which, if no data received from server, ssh sends a message requesting response. Default 0 (disabled) [1][5][6][2]. - ServerAliveCountMax: Number of unanswered server alive messages before disconnecting. Default 3. E.g., with ServerAliveInterval 15, disconnects after ~45s [1][5][6][2]. These can be set in ~/.ssh/config: Host * ConnectTimeout 10 ServerAliveInterval 60 ServerAliveCountMax 3 [7][2]. sshpass has no built-in timeout option; it runs the ssh command non-interactively with password. Use ssh options like ConnectTimeout via sshpass -p pass ssh -o ConnectTimeout=10 user@host, or wrap with timeout(1): timeout 30 sshpass... [8][9][7][10]. For overall command timeout, use the timeout utility from coreutils [7].

Citations:


Add SSH/SCP connection timeouts to prevent hung deploy jobs.

These external network calls currently have no connect/session timeout guards, so transient network failures can block runners indefinitely. Add timeout protection to all three ssh/scp invocations:

Suggested change
-          sshpass -e ssh -o StrictHostKeyChecking=no \
+          sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
             ${{ secrets.USERNAME }}@${{ secrets.HOST }} \
             "mkdir -p /home/${{ secrets.USERNAME }}/tmp/nestjs"
...
-          sshpass -e scp -o StrictHostKeyChecking=no \
+          sshpass -e scp -o StrictHostKeyChecking=no -o ConnectTimeout=30 \
             ${{ inputs.tarball }} \
             ${{ secrets.USERNAME }}@${{ secrets.HOST }}:${{ env.REMOTE_TMP }}/
...
-          sshpass -e ssh -o StrictHostKeyChecking=no \
+          sshpass -e ssh -o StrictHostKeyChecking=no -o ConnectTimeout=30 -o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
             ${{ secrets.USERNAME }}@${{ secrets.HOST }} << REMOTE
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/_deploy.yml around lines 55 - 57, The ssh/scp invocations
(e.g., the ssh call using "sshpass -e ssh" that runs "mkdir -p /home/${{
secrets.USERNAME }}/tmp/nestjs" and the other two scp/ssh calls) lack timeouts
and can hang; update each invocation to include SSH connection/session timeouts
and a bounded overall command timeout: add SSH options like -o ConnectTimeout=10
-o ConnectionAttempts=2 -o ServerAliveInterval=15 -o ServerAliveCountMax=2 to
the ssh/scp commands, and wrap the whole invocation with a timeout (e.g., prefix
with "timeout 60s") so the pipeline fails fast on network issues; apply these
changes to every occurrence of "sshpass -e ssh" and "scp -o ..." in the
workflow.

Comment on lines +3 to +7
on:
workflow_dispatch:
workflow_call:

jobs:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/_security.yml

Repository: hngprojects/flowbrand-be

Length of output: 1181


🏁 Script executed:

rg -r '${{ jobs.workflow_call' --type=yaml .github/workflows/ || rg 'uses:.*_security' --type=yaml .github/workflows/

Repository: hngprojects/flowbrand-be

Length of output: 143


🏁 Script executed:

cat -n .github/workflows/pipeline.yml

Repository: hngprojects/flowbrand-be

Length of output: 6666


Add explicit least-privilege permissions to this reusable security workflow.

This workflow lacks explicit permissions declaration and should rely on repository defaults. Since both jobs (audit and secret-scan) only perform read-only operations (repository checkout and scanning), explicitly declaring contents: read enforces the principle of least privilege and is a security best practice for reusable workflows.

Suggested change
 on:
   workflow_dispatch:
   workflow_call:
+
+permissions:
+  contents: read
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/_security.yml around lines 3 - 7, Add an explicit
least-privilege permissions block to the reusable workflow by declaring
permissions: contents: read at the top-level of the workflow (alongside on:), so
both jobs (audit and secret-scan) run with read-only repo access; update the
workflow file to include this permissions declaration to enforce least privilege
for the reusable workflow.

Comment on lines +24 to +25
- name: Run npm audit
run: npm audit --audit-level=high --omit=dev

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

# First, locate the security workflow file
find . -name "_security.yml" -o -name "*security*.yml" | head -20

Repository: hngprojects/flowbrand-be

Length of output: 102


🏁 Script executed:

# Also check the general workflow directory structure
git ls-files .github/workflows/ | head -20

Repository: hngprojects/flowbrand-be

Length of output: 246


🏁 Script executed:

# Let's search for the specific file mentioned
cat -n .github/workflows/_security.yml 2>/dev/null || echo "File not found, searching for it..."

Repository: hngprojects/flowbrand-be

Length of output: 1181


🏁 Script executed:

# Let's check what the purpose of this repo is and understand the context better
head -50 package.json 2>/dev/null | cat -n

Repository: hngprojects/flowbrand-be

Length of output: 2521


🏁 Script executed:

# Let's also check if there are any comments or documentation explaining the --omit=dev choice
git log --oneline -n 20 -- .github/workflows/_security.yml 2>/dev/null || echo "No git history available"

Repository: hngprojects/flowbrand-be

Length of output: 106


🏁 Script executed:

# Check if dev dependencies are used in CI or if this is primarily a library
git ls-files | grep -E "(Dockerfile|docker-compose|tsconfig|jest|lint)" | head -10

Repository: hngprojects/flowbrand-be

Length of output: 222


🌐 Web query:

npm audit --omit=dev behavior what does it do

💡 Result:

The npm audit --omit=dev command runs a security audit on your project's dependencies but excludes devDependencies from the audit report. By default, npm audit checks direct dependencies, devDependencies, bundledDependencies, and optionalDependencies (but not peerDependencies) [1][2]. The --omit=dev flag instructs npm to omit "dev" dependency types from the submitted payload to the audit endpoint, so omitted dependency types (like devDependencies) are skipped when generating the vulnerability report [3][4][5]. This means vulnerabilities in devDependencies are not reported, focusing the audit on production dependencies instead [6]. This flag is part of npm's general --omit option, which can also target "optional" or "peer" types, and is preferred over the deprecated --production flag in recent npm versions [6].

Citations:


🌐 Web query:

npm audit dev dependencies security concerns CI/CD

💡 Result:

npm audit checks both production dependencies and devDependencies by default for security vulnerabilities.[1][2] In CI/CD pipelines, a key concern with devDependencies is noisy output from "phantom vulnerabilities" that don't affect production runtime, potentially blocking builds unnecessarily.[3] To address this, use npm audit --omit=dev (preferred over deprecated --production) to audit only production dependencies, reducing false alarms while gating on critical/high risks.[3][4][5] However, devDependencies pose real security risks in CI/CD: they execute during builds on servers with access to secrets, code, and deployment credentials, enabling potential exfiltration or tampering if compromised.[6] Supply chain attacks via dev tools can leak production data.[7] Thus, best practices balance both: - Run npm ci (not npm install) in CI for deterministic installs.[3][8] - Audit production deps strictly: npm audit --omit=dev --audit-level=high and fail on high/critical.[3][5][9] - Optionally audit full tree separately or use tools like audit-ci with --skip-dev flag, allowlists, and thresholds.[9][10] - GitHub Actions examples: Use npm-audit-action with production_flag: true or audit-ci to fail builds.[11][12] - Regularly update all deps, review lockfile changes in PRs, and monitor for new advisories.[3][8] npm audit has limits (e.g., misses unknown supply chain attacks[13]), so combine with lockfile scanning and monitoring.[8][14]

Citations:


Audit excludes dev dependencies despite installing them, creating a security blind spot.

Line 22 installs dev dependencies with --include=dev, but line 25 runs npm audit --omit=dev, which skips vulnerability checks on those dependencies. Dev tools execute during CI builds on servers with access to secrets and credentials, making them part of the supply chain attack surface. Run the full audit to cover the complete dependency tree.

Suggested change
      - name: Run npm audit
-       run: npm audit --audit-level=high --omit=dev
+       run: npm audit --audit-level=high
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
- name: Run npm audit
run: npm audit --audit-level=high --omit=dev
- name: Run npm audit
run: npm audit --audit-level=high
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/_security.yml around lines 24 - 25, The CI step named "Run
npm audit" currently invokes "npm audit --audit-level=high --omit=dev" which
skips auditing dev dependencies even though the workflow installs dev deps;
update that step to run a full audit by removing the "--omit=dev" flag (i.e.,
run "npm audit --audit-level=high") so the audit covers the entire dependency
tree including dev dependencies and CI-only tools used in the build.

Comment on lines +12 to +13
- name: Checkout code
uses: actions/checkout@v4

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

What are the current default GITHUB_TOKENpermissions in GitHub Actions, and is setting explicitpermissionspluspersist-credentials: false recommended for CI jobs that only need repository read access?

💡 Result:

Current default GITHUB_TOKEN permissions (2026-05-10) 1) By default for a new personal-account repository: GITHUB_TOKEN has read access for the contents and packages scopes only. [1] 2) For orgs/repositories where the “restricted” default is selected: the default is “read access for the contents and packages permissions”; GitHub also notes this can be inherited from organization settings. [2] 3) If the “permissive” option is selected (“read and write access for all permissions”), then the default includes write access across scopes. [2] So, the “current default” depends on your repository/org setting; the documentation explicitly calls out contents + packages as the restricted default for new personal repos, and the alternative as read/write for all scopes. [2] Is setting explicit permissions plus persist-credentials: false recommended for CI jobs that only need repository read access? A) Explicit permissions: Yes, as a security best practice. GitHub’s docs recommend granting the GITHUB_TOKEN the least access required and using the workflow/job-level permissions key to do so. [3][4][5] Example for a job that only needs to read the repository: permissions: contents: read packages: read # (optionally set other scopes to none if you know you don't need them) This matches GitHub’s guidance to limit GITHUB_TOKEN access. [3][4][5] Also note: the GITHUB_TOKEN is automatically created per job. [6] B) persist-credentials: false: Often recommended when using actions/checkout, but it is about credential persistence in the runner’s git config, not about GITHUB_TOKEN API permission scopes. GitHub’s docs for GITHUB_TOKEN permissions focus on the permissions key (scopes granted to the token) rather than on checkout credential persistence. [3][4][6] Separate from that, the actions/checkout ecosystem has security-oriented guidance to use persist-credentials: false to avoid persisting credentials in git config after checkout. For example, a GitHub-related PR explicitly states it “prevents GITHUB_TOKEN from persisting in git config after checkout by always emitting persist-credentials: false.” [7] Conclusion: for CI jobs that only need repository read access, it’s recommended to (1) set explicit permissions with least-privilege GITHUB_TOKEN scope (e.g., contents: read and typically packages: read), per GitHub guidance. [3][4][5] Additionally, when you use actions/checkout, setting persist-credentials: false is a sensible hardening measure to reduce how long credentials are persisted in git configuration on the runner, as reflected in security-focused checkout guidance. [7]

Citations:


🏁 Script executed:

cat .github/workflows/_test.yml

Repository: hngprojects/flowbrand-be

Length of output: 537


Harden GitHub token exposure in the test job.

The workflow lacks explicit permissions declaration and the checkout step doesn't disable credential persistence. This increases risk if a dependency script is compromised during npm install.

Add workflow-level permissions: contents: read and configure the checkout step with persist-credentials: false to minimize token exposure and reduce git credential persistence on the runner.

🔐 Suggested hardening patch
 name: _test
 
 on:
   workflow_dispatch:
   workflow_call:
 
+permissions:
+  contents: read
+
 jobs:
   test:
     name: Test
     runs-on: ubuntu-latest
     steps:
       - name: Checkout code
         uses: actions/checkout@v4
+        with:
+          persist-credentials: false
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/_test.yml around lines 12 - 13, Add least-privilege
workflow permissions and prevent checkout from persisting GitHub credentials: at
the top-level of the workflow add a permissions block with contents: read (i.e.,
permissions: contents: read) and update the actions/checkout@v4 step (the
"Checkout code" step) to include persist-credentials: false so the runner won't
retain the GITHUB_TOKEN for subsequent commands (reducing token exposure during
npm install or other scripts).

Comment on lines +115 to +119
if: |
github.event_name != 'pull_request' && (
(github.event_name == 'push' && github.ref == 'refs/heads/dev') ||
(github.event_name == 'workflow_dispatch' && inputs.environment == 'dev')
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

cat -n .github/workflows/pipeline.yml | head -160

Repository: hngprojects/flowbrand-be

Length of output: 6456


🏁 Script executed:

# Check if there are any branch protection or environment rules configured
find . -name '*.yml' -o -name '*.yaml' | head -20

Repository: hngprojects/flowbrand-be

Length of output: 474


🌐 Web query:

GitHub Actions workflow_dispatch github.ref context available branch

💡 Result:

Yes, the github.ref context is available in GitHub Actions workflows triggered by workflow_dispatch. It contains the fully-formed ref (e.g., refs/heads/main) of the branch or tag specified when dispatching the workflow.[1][2][3] When manually triggering a workflow via the GitHub UI, CLI (gh workflow run --ref BRANCH), or REST API, you select or provide a ref parameter, which becomes the value of github.ref (and the $GITHUB_REF environment variable) in the workflow run.[4][5][6] The workflow file must exist on the specified branch for the dispatch to succeed, but typically workflows are defined on the default branch.[4][2] github.ref_name provides the short branch or tag name (e.g., main).[1] Example usage in a workflow: if: github.ref == 'refs/heads/main' or run: echo "Branch: ${{ github.ref_name }}" This behavior is consistent across official documentation as of 2026.[1][4][2][3][7]

Citations:


Manual deploy conditions lack branch enforcement (unreviewed code can reach production via workflow_dispatch).

The deploy jobs only validate inputs.environment for manual triggers, not the branch. This allows deploying artifacts built from any branch to any environment when triggering workflow_dispatch, contingent only on GitHub Environment approval gates rather than enforced branch restrictions.

Suggested change
   deploy-staging:
     if: |
       github.event_name != 'pull_request' && (
         (github.event_name == 'push'              && github.ref == 'refs/heads/staging') ||
-        (github.event_name == 'workflow_dispatch' && inputs.environment == 'staging')
+        (github.event_name == 'workflow_dispatch' && inputs.environment == 'staging' && github.ref == 'refs/heads/staging')
       )
...
   deploy-prod:
     if: |
       github.event_name != 'pull_request' && (
         (github.event_name == 'push'              && github.ref == 'refs/heads/main') ||
-        (github.event_name == 'workflow_dispatch' && inputs.environment == 'prod')
+        (github.event_name == 'workflow_dispatch' && inputs.environment == 'prod' && github.ref == 'refs/heads/main')
       )

Also applies to: 118 (deploy-dev should enforce github.ref == 'refs/heads/dev')

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/pipeline.yml around lines 115 - 119, The workflow's manual
trigger condition allows workflow_dispatch from any branch because it only
checks inputs.environment; update the if condition for the deploy jobs (e.g.,
the block referencing github.event_name, github.ref, inputs.environment and the
deploy-dev job) to require both inputs.environment == 'dev' AND github.ref ==
'refs/heads/dev' for workflow_dispatch paths (similarly enforce the correct
branch for other environments), so manual deployments only run when the trigger
comes from the matching branch as well as the matching environment input.

@BAGHIRA-0F-RELIGIONS
BAGHIRA-0F-RELIGIONS merged commit ece1bb0 into hngprojects:dev May 10, 2026
9 checks passed
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants