---
title: "Deploy your first change with GitHub Actions"
description: "Deploy your first change with GitHub Actions. Create a scoped service token, set repository secrets, run a workflow, and verify its command record in Alpacon."
---

> Full documentation index: https://docs.alpacax.com/llms.txt

# Deploy your first change with GitHub Actions

<span id="building-cicd-pipeline-with-github-actions" />

# Deploy your first change 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

- [Alpacon workspace ready](/learn/quickstart/#step-1-get-a-workspace)
- [Server registered and agent installed](/learn/quickstart/#step-2-register-your-first-server-2-min)
- An [application](/use/iam/applications/) for this pipeline, or administrator access to create one
- GitHub repository access

## 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 **Tokens** tab and click **Create token**

Work through the four steps:

```yaml
Purpose: name github-actions-deploy, expires in 30 days
Permissions: start from the Deploy pipeline template, then adjust
Access rules: prod-server and the upload path listed in 1-2
Review: write why this pipeline needs the token
```

Expiry defaults to 30 days, and you can pick 90 days, 365 days, or a custom time instead. Service tokens must expire, but the workspace maximum token lifetime does not apply to them.

The key appears once, hidden, when you select **Create token**. It starts with `alpst-`. Copy or download it, then check **I've stored the key somewhere safe** to close the dialog.

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 its ACL rules are applied. Servers, commands, and file paths are each deny-by-default, so a token with no rules is refused everywhere.

The rules you chose in the **Access rules** step wait on the token until it is approved. After approval, open the token's **Access rules** tab and select **Apply** under **Prepared access rules**, or add the server and file rules there yourself. On Alpacon Cloud, a **Command ACL admins** member adds each command below under **Pre-approved commands** on the same tab. This access is separate from the workspace role. On self-hosted deployments, add each line as a command pattern under **Allowed commands** on the **Access rules** tab. Configure:

| Rule | What this pipeline needs |
|------|--------------------------|
| Server ACL | `prod-server` |
| Command access | The command lines below |
| File ACL | `/var/www/app/dist/`, the target of the build upload |

Each command entry must match a whole submitted line. For the workflows below, add the commands each token needs, one per entry:

```text
pm2 restart app
pm2 status
curl -f http://localhost:3000/health || exit 1
cd /app && git pull && npm ci && npm run build && pm2 restart app
cd /app && if [ "$ROLLBACK_VERSION" = "previous" ]; then git reset --hard HEAD~1; else git reset --hard "$ROLLBACK_VERSION"; fi && npm ci && npm run build && pm2 restart app
```

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.

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:

| Name | Value |
|------|-------|
| `ALPACON_WORKSPACE_URL` | https://your-workspace.us1.alpacon.io |
| `ALPACON_SERVICE_TOKEN` | The `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`:
```yaml
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

```bash
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`:
```yaml
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`:
```yaml
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
      env:
        ROLLBACK_VERSION: ${{ github.event.inputs.version }}
      with:
        workspace-url: ${{ secrets.ALPACON_WORKSPACE_URL }}
        api-token: ${{ secrets.ALPACON_SERVICE_TOKEN }}
        target: 'prod-server'
        env: |
          ROLLBACK_VERSION
        script: |
          cd /app && if [ "$ROLLBACK_VERSION" = "previous" ]; then git reset --hard HEAD~1; else git reset --hard "$ROLLBACK_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

| Issue | Cause | Solution |
|-------|-------|----------|
| `Permission denied` | No matching Command ACL entry; an empty Command ACL denies every command | Add the entry on the token's **Access rules** tab |
| Upload step refused | Server ACL or File ACL missing for the target path | Add both; a transfer is checked against the Server ACL, then the File ACL |
| Authentication fails right after creation | The token is still **pending** and was never activated | Ask an approver to activate it |
| `Server not found` | Wrong server name | Verify server name in workspace |
| `Token expired` | Token expired | Issue a new token and update the GitHub secret |

## Next steps

- [Deployment overview](/integrate/)
- [Service tokens](/use/iam/applications/service-tokens/)
- [Security best practices](/learn/best-practices/security/)
- [Monitoring and alerting setup](/learn/best-practices/monitoring/)

## 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.

Source: https://docs.alpacax.com/learn/tutorials/github-actions-setup/
