Building CI/CD pipeline with GitHub Actions

Step-by-step tutorial for building a CI/CD pipeline using Alpacon GitHub Actions.

Learning objectives

After completing this tutorial, you’ll be able to:

  • Build secure deployment pipelines without SSH keys
  • Implement fine-grained command-level access control
  • Create automated build and deployment processes

Prerequisites

Step 1: Create a service token (5 min + approval wait)

1-1. Create a service token

CI and automation run on service tokens. A service token belongs to an application rather than to a person, so it survives staff changes, and its ACL bounds exactly what the pipeline may do.

  1. Log in to your Alpacon workspace
  2. Go to IAM → Applications and open the application this pipeline belongs to, or create one
  3. Open the Credentials tab and click Add

In the form, set:

Token name: github-actions-deploy
Expiration: 3 months (recommended)
Scopes: select the scopes the deployment needs
Reason: why this pipeline needs the token

Copy the key when it is shown. It starts with alpst-, and it is shown only once.

A token scoped for CI is reviewed before it works: the request is created as pending and authenticates nowhere until someone with approval rights activates it. Allow for that step rather than discovering it mid-pipeline.

1-2. Add the ACL rules

A new service token reaches nothing until you add ACL rules. Servers, commands, and file paths are each deny-by-default, so a token with no rules is refused everywhere.

Open the token, go to its ACL tab, and add:

RuleWhat this pipeline needs
Server ACLprod-server
Command ACLThe command patterns below
File ACL/var/www/app/dist/, the target of the build upload

Each Command ACL entry is a command pattern (wildcards * supported), one per entry:

git *
npm *
pm2 *
curl *
systemctl status nginx

A file transfer checks the server rule first, then the file rule, so the Upload to server step needs a Server ACL entry and a File ACL entry, not just one of them.

💡 Tip: Separate development and production tokens for better management.

1-3. Confirm the token is active

Open the token’s Overview tab and check that its status is no longer pending. A pending token authenticates nowhere, so a pipeline built on it fails at its first Alpacon step.

Step 2: Configure GitHub repository (3 min)

2-1. Add GitHub Secrets

In your GitHub repository:

  1. Go to Settings > Secrets and variables > Actions
  2. Click New repository secret
  3. Add the following secrets:
NameValue
ALPACON_WORKSPACE_URLhttps://your-workspace.us1.alpacon.io
ALPACON_SERVICE_TOKENThe alpst- key from Step 1

The action input below is named api-token and takes the service token as-is. The name is kept for compatibility with published workflows.

2-2. Create workflow file

Create .github/workflows directory in repository root:

.github/workflows/deploy.yml:

name: Deploy to Production
 
on:
  push:
    branches: [main]
 
env:
  NODE_VERSION: '18'
 
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
    - uses: actions/checkout@v4
 
    - name: Setup Node.js
      uses: actions/setup-node@v4
      with:
        node-version: ${{ env.NODE_VERSION }}
        cache: 'npm'
 
    - name: Install dependencies
      run: npm ci
 
    - name: Run tests
      run: npm test
 
    - name: Build application
      run: npm run build
 
    # Save build artifacts
    - name: Upload build artifacts
      uses: actions/upload-artifact@v4
      with:
        name: build-artifacts
        path: dist/
 
  deploy:
    needs: test
    runs-on: ubuntu-latest
    if: github.ref == 'refs/heads/main'
 
    steps:
    # Download build artifacts
    - name: Download artifacts
      uses: actions/download-artifact@v4
      with:
        name: build-artifacts
        path: dist/
 
    # Install Alpacon CLI
    - name: Setup Alpacon CLI
      uses: alpacax/alpacon-setup-action@v1
 
    # Upload build to server
    - name: Upload to server
      uses: alpacax/alpacon-cp-action@v1
      with:
        workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
        api-token: ${{ secrets.ALPACON_SERVICE_TOKEN }}
        source: './dist/'
        target-server: 'prod-server'
        target-path: '/var/www/app/dist/'
        recursive: true
 
    # Restart application
    - name: Restart application
      uses: alpacax/alpacon-websh-action@v1
      with:
        workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
        api-token: ${{ secrets.ALPACON_SERVICE_TOKEN }}
        target: 'prod-server'
        script: |
          pm2 restart app
          pm2 status
 
    # Health check
    - name: Health check
      uses: alpacax/alpacon-websh-action@v1
      with:
        workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
        api-token: ${{ secrets.ALPACON_SERVICE_TOKEN }}
        target: 'prod-server'
        script: |
          curl -f http://localhost:3000/health || exit 1

Step 3: Run first deployment (2 min)

3-1. Push code

git add .github/workflows/deploy.yml
git commit -m "Add CI/CD pipeline with Alpacon"
git push origin main

3-2. Monitor deployment

  1. Go to Actions tab in GitHub repository
  2. Click on running workflow
  3. Check logs for each step

3-3. Verify in Alpacon

  1. Open Audit > Events in your Alpacon workspace
  2. Check the Commands tab for executed commands and results, recorded against the application that owns the token
  3. Open the token’s Overview tab, where its usage is recorded

Step 4: Advanced configuration (Optional)

Multi-environment deployment

.github/workflows/deploy-multi-env.yml:

name: Multi-Environment Deploy
 
on:
  push:
    branches: [main, develop]
 
jobs:
  deploy:
    runs-on: ubuntu-latest
 
    steps:
    - uses: actions/checkout@v4
 
    - name: Setup Alpacon CLI
      uses: alpacax/alpacon-setup-action@v1
 
    # Set environment based on branch
    - name: Set environment
      id: env
      run: |
        if [[ "${{ github.ref }}" == "refs/heads/main" ]]; then
          echo "target=prod-server" >> $GITHUB_OUTPUT
          echo "token=${{ secrets.ALPACON_PROD_TOKEN }}" >> $GITHUB_OUTPUT
        else
          echo "target=dev-server" >> $GITHUB_OUTPUT
          echo "token=${{ secrets.ALPACON_DEV_TOKEN }}" >> $GITHUB_OUTPUT
        fi
 
    - name: Deploy to ${{ steps.env.outputs.target }}
      uses: alpacax/alpacon-websh-action@v1
      with:
        workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
        api-token: ${{ steps.env.outputs.token }}
        target: ${{ steps.env.outputs.target }}
        script: |
          cd /app
          git pull
          npm ci
          npm run build
          pm2 restart app

Each of these tokens carries its own ACL, so the development token never needs a Server ACL entry for prod-server.

Rollback workflow

.github/workflows/rollback.yml:

name: Rollback Deployment
 
on:
  workflow_dispatch:
    inputs:
      version:
        description: 'Version to rollback to'
        required: true
        default: 'previous'
 
jobs:
  rollback:
    runs-on: ubuntu-latest
 
    steps:
    - name: Setup Alpacon CLI
      uses: alpacax/alpacon-setup-action@v1
 
    - name: Rollback application
      uses: alpacax/alpacon-websh-action@v1
      with:
        workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
        api-token: ${{ secrets.ALPACON_SERVICE_TOKEN }}
        target: 'prod-server'
        script: |
          cd /app
          if [ "${{ github.event.inputs.version }}" = "previous" ]; then
            git reset --hard HEAD~1
          else
            git reset --hard ${{ github.event.inputs.version }}
          fi
          npm ci
          npm run build
          pm2 restart app

Troubleshooting

When deployment fails

  1. Check GitHub Actions logs

    • Check failed step in Actions tab
    • Review error messages
  2. Check Alpacon audit events

    • Review command execution logs under Audit > Events
    • Verify server connection status
  3. Check the token’s ACL and status

    • Ensure the command has a matching Command ACL entry, and that a file transfer has both a Server ACL and a File ACL entry
    • Check that the token is active rather than still pending, and that it hasn’t expired

Common issues

IssueCauseSolution
Permission deniedNo matching Command ACL entry; an empty Command ACL denies every commandAdd the entry on the token’s ACL tab
Upload step refusedServer ACL or File ACL missing for the target pathAdd both; a transfer is checked against the Server ACL, then the File ACL
Authentication fails right after creationThe token is still pending and was never activatedAsk an approver to activate it
Server not foundWrong server nameVerify server name in workspace
Token expiredToken expiredIssue a new token and update the GitHub secret

Next steps

Summary

In this tutorial, you completed:

✅ Service token creation and ACL configuration ✅ GitHub Secrets setup ✅ CI/CD pipeline creation ✅ Automated deployment execution and monitoring

You’ve built a safer and more manageable deployment pipeline using a scoped service token instead of SSH keys.

Last updated: