GitLab CI Integration Recipe
Implementation Reference
Source Files:
packages/core/src/core/execution-engine.ts
- Core execution enginepackages/core/src/adapters/docker-adapter.ts
- Docker executionapps/xec/src/commands/run.ts
- Script execution
Key Functions:
$.execute()
- Command executionDockerAdapter.execute()
- Container executionRunCommand.execute()
- Script runner
Overview
This recipe demonstrates how to integrate Xec with GitLab CI/CD pipelines for automated testing, building, and deployment workflows.
Basic GitLab CI Configuration
Simple Pipeline with Xec
# .gitlab-ci.yml
image: node:18-alpine
stages:
- install
- test
- build
- deploy
variables:
XEC_VERSION: "latest"
XEC_CACHE_DIR: "$CI_PROJECT_DIR/.xec-cache"
before_script:
- npm install -g @xec-sh/cli@${XEC_VERSION}
- xec --version
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- node_modules/
- .xec-cache/
install:dependencies:
stage: install
script:
- npm ci
- xec run scripts/setup.ts
artifacts:
paths:
- node_modules/
expire_in: 1 hour
test:unit:
stage: test
script:
- xec test:unit
coverage: '/Coverage: \d+\.\d+%/'
artifacts:
reports:
junit: test-results.xml
coverage_report:
coverage_format: cobertura
path: coverage/cobertura-coverage.xml
test:integration:
stage: test
services:
- docker:dind
variables:
DOCKER_HOST: tcp://docker:2375
script:
- xec test:integration --docker
allow_failure: true
build:application:
stage: build
script:
- xec build --env=production
artifacts:
paths:
- dist/
expire_in: 1 week
deploy:staging:
stage: deploy
environment:
name: staging
url: https://staging.example.com
script:
- xec deploy staging --auto-approve
only:
- develop
deploy:production:
stage: deploy
environment:
name: production
url: https://example.com
script:
- xec deploy production --confirm
when: manual
only:
- main
Advanced Pipeline Configuration
Multi-Environment Deployment
# .gitlab-ci.yml
image: node:18
stages:
- validate
- test
- build
- deploy
- verify
variables:
XEC_CONFIG: ".xec/config.yaml"
DOCKER_DRIVER: overlay2
DOCKER_TLS_CERTDIR: "/certs"
.xec_template: &xec_setup
before_script:
- apt-get update && apt-get install -y curl jq
- npm install -g @xec-sh/cli
- echo "XEC_CONFIG_PATH=${XEC_CONFIG}" >> .env
- |
cat > .xec/config.yaml << EOF
targets:
staging:
type: ssh
host: ${STAGING_HOST}
user: ${STAGING_USER}
privateKey: ${STAGING_SSH_KEY}
production:
type: ssh
host: ${PROD_HOST}
user: ${PROD_USER}
privateKey: ${PROD_SSH_KEY}
EOF
validate:config:
stage: validate
<<: *xec_setup
script:
- xec config validate
- xec inspect --targets
only:
changes:
- .xec/config.yaml
- .gitlab-ci.yml
test:parallel:
stage: test
<<: *xec_setup
parallel:
matrix:
- TEST_SUITE: [unit, integration, e2e]
script:
- xec test:${TEST_SUITE} --parallel
artifacts:
when: always
reports:
junit: test-results-${TEST_SUITE}.xml
paths:
- coverage/
build:docker:
stage: build
image: docker:latest
services:
- docker:dind
<<: *xec_setup
script:
- |
cat > build-docker.ts << 'EOF'
import { $ } from '@xec-sh/core';
const version = process.env.CI_COMMIT_SHORT_SHA;
const registry = process.env.CI_REGISTRY;
const image = `${registry}/${process.env.CI_PROJECT_PATH}`;
async function buildAndPush() {
// Build image
await $`docker build -t ${image}:${version} .`;
await $`docker tag ${image}:${version} ${image}:latest`;
// Login to registry
await $`echo ${process.env.CI_REGISTRY_PASSWORD} | docker login -u ${process.env.CI_REGISTRY_USER} --password-stdin ${registry}`;
// Push images
await $`docker push ${image}:${version}`;
await $`docker push ${image}:latest`;
console.log(`✅ Pushed ${image}:${version}`);
}
buildAndPush().catch(console.error);
EOF
- xec run build-docker.ts
only:
- main
- develop
deploy:review:
stage: deploy
<<: *xec_setup
environment:
name: review/$CI_COMMIT_REF_NAME
url: https://$CI_COMMIT_REF_SLUG.review.example.com
on_stop: stop:review
auto_stop_in: 2 days
script:
- |
cat > deploy-review.ts << 'EOF'
import { $ } from '@xec-sh/core';
const branch = process.env.CI_COMMIT_REF_NAME;
const slug = process.env.CI_COMMIT_REF_SLUG;
async function deployReview() {
// Create review environment
await $.ssh('staging')`
docker run -d \
--name review-${slug} \
-e BRANCH=${branch} \
-p 0:3000 \
${process.env.CI_REGISTRY_IMAGE}:${process.env.CI_COMMIT_SHORT_SHA}
`;
// Get assigned port
const port = await $.ssh('staging')`docker port review-${slug} 3000 | cut -d: -f2`.stdout.trim();
// Update proxy configuration
await $.ssh('staging')`
echo "location /${slug}/ { proxy_pass http://localhost:${port}/; }" > /etc/nginx/sites-available/review-${slug}
ln -sf /etc/nginx/sites-available/review-${slug} /etc/nginx/sites-enabled/
nginx -s reload
`;
console.log(`✅ Review app deployed at https://${slug}.review.example.com`);
}
deployReview().catch(console.error);
EOF
- xec run deploy-review.ts
only:
- merge_requests
stop:review:
stage: deploy
<<: *xec_setup
environment:
name: review/$CI_COMMIT_REF_NAME
action: stop
script:
- |
xec on staging "
docker stop review-${CI_COMMIT_REF_SLUG} || true
docker rm review-${CI_COMMIT_REF_SLUG} || true
rm -f /etc/nginx/sites-enabled/review-${CI_COMMIT_REF_SLUG}
nginx -s reload
"
when: manual
only:
- merge_requests
deploy:production:
stage: deploy
<<: *xec_setup
environment:
name: production
url: https://example.com
script:
- |
cat > deploy-production.ts << 'EOF'
import { $ } from '@xec-sh/core';
async function deployProduction() {
const version = process.env.CI_COMMIT_TAG || process.env.CI_COMMIT_SHORT_SHA;
const targets = ['prod-web-1', 'prod-web-2', 'prod-web-3'];
// Health check before deployment
for (const target of targets) {
const health = await $.ssh(target)`curl -f http://localhost/health`.nothrow();
if (!health.ok) {
throw new Error(`Health check failed for ${target}`);
}
}
// Rolling deployment
for (const target of targets) {
console.log(`Deploying to ${target}...`);
// Remove from load balancer
await $.ssh('prod-lb')`/usr/local/bin/remove-backend ${target}`;
// Deploy new version
await $.ssh(target)`
docker pull ${process.env.CI_REGISTRY_IMAGE}:${version}
docker stop app || true
docker rm app || true
docker run -d --name app -p 80:3000 ${process.env.CI_REGISTRY_IMAGE}:${version}
`;
// Wait for health check
for (let i = 0; i < 30; i++) {
const health = await $.ssh(target)`curl -f http://localhost/health`.nothrow();
if (health.ok) break;
await new Promise(resolve => setTimeout(resolve, 2000));
}
// Add back to load balancer
await $.ssh('prod-lb')`/usr/local/bin/add-backend ${target}`;
console.log(`✅ ${target} deployed successfully`);
}
}
deployProduction().catch(console.error);
EOF
- xec run deploy-production.ts
rules:
- if: '$CI_COMMIT_TAG'
when: manual
- if: '$CI_COMMIT_BRANCH == "main"'
when: manual
needs:
- build:docker
- test:parallel
verify:deployment:
stage: verify
<<: *xec_setup
script:
- |
cat > verify-deployment.ts << 'EOF'
import { $ } from '@xec-sh/core';
async function verifyDeployment() {
const environment = process.env.CI_ENVIRONMENT_NAME;
const url = process.env.CI_ENVIRONMENT_URL;
console.log(`Verifying ${environment} deployment at ${url}`);
// Check HTTP status
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
// Check version endpoint
const versionResponse = await fetch(`${url}/api/version`);
const version = await versionResponse.json();
console.log('Deployed version:', version);
// Run smoke tests
await $`npm run test:smoke -- --url ${url}`;
// Check metrics
const metricsResponse = await fetch(`${url}/metrics`);
const metrics = await metricsResponse.text();
// Verify key metrics
if (!metrics.includes('http_requests_total')) {
throw new Error('Metrics endpoint not working correctly');
}
console.log('✅ Deployment verification passed');
}
verifyDeployment().catch(console.error);
EOF
- xec run verify-deployment.ts
needs:
- deploy:production