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
- Server registered and agent installed
- An application 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.
- Log in to your Alpacon workspace
- Go to IAM → Applications and open the application this pipeline belongs to, or create one
- Open the Tokens tab and click Create token
Work through the four steps:
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 tokenExpiry 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:
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 appA 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:
- Go to Settings > Secrets and variables > Actions
- Click New repository secret
- 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:
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 1Step 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 main3-2. Monitor deployment
- Go to Actions tab in GitHub repository
- Click on running workflow
- Check logs for each step
3-3. Verify in Alpacon
- Open Audit > Events in your Alpacon workspace
- Check the Commands tab for executed commands and results, recorded against the application that owns the token
- 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 appEach 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
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 appTroubleshooting
When deployment fails
-
Check GitHub Actions logs
- Check failed step in Actions tab
- Review error messages
-
Check Alpacon audit events
- Review command execution logs under Audit > Events
- Verify server connection status
-
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
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.