Skip to content

feat: enhance PACS008 agent fields with full CBPR+ compliance and fix… #64

feat: enhance PACS008 agent fields with full CBPR+ compliance and fix…

feat: enhance PACS008 agent fields with full CBPR+ compliance and fix… #64

Workflow file for this run

name: Automated Azure Deployment
permissions:
contents: write
issues: write
on:
push:
branches: [ main ]
pull_request:
branches: [ main ]
workflow_dispatch:
inputs:
environment:
description: 'Environment to deploy to'
required: true
default: 'production'
type: choice
options:
- production
- staging
force_setup:
description: 'Force Azure infrastructure setup'
required: false
default: false
type: boolean
version_bump:
description: 'Version bump type'
required: false
default: 'patch'
type: choice
options:
- patch
- minor
- major
skip_release:
description: 'Skip creating GitHub release'
required: false
default: false
type: boolean
env:
RESOURCE_GROUP: rg-reframe-prod
LOCATION: eastus
IMAGE_NAME: reframe
CONTAINER_NAME: reframe-api
SERVICE_PRINCIPAL_NAME: sp-reframe-cicd
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
- name: Cache cargo dependencies
uses: actions/cache@v3
with:
path: |
~/.cargo/registry
~/.cargo/git
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Format check
run: cargo fmt -- --check
- name: Clippy check
run: cargo clippy -- -D warnings
- name: Run tests
run: cargo test
version-and-release:
runs-on: ubuntu-latest
needs: test
if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
outputs:
version: ${{ steps.version.outputs.version }}
tag: ${{ steps.version.outputs.tag }}
changelog: ${{ steps.version.outputs.changelog }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- name: Configure Git
run: |
git config --local user.email "action@github.com"
git config --local user.name "GitHub Action"
- name: Install cargo-edit for version bumping
run: cargo install cargo-edit
- name: Get current version
id: current-version
run: |
CURRENT_VERSION=$(grep '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
echo "current=$CURRENT_VERSION" >> $GITHUB_OUTPUT
echo "Current version: $CURRENT_VERSION"
- name: Determine version bump type
id: bump-type
run: |
if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then
BUMP_TYPE="${{ github.event.inputs.version_bump }}"
else
# Auto-determine based on commit messages
if git log --format=%B -n 20 | grep -qE '\[major\]|\bbreaking\b|\bBREAKING\b'; then
BUMP_TYPE="major"
elif git log --format=%B -n 20 | grep -qE '\[minor\]|\bfeat\b|\bfeature\b'; then
BUMP_TYPE="minor"
else
BUMP_TYPE="patch"
fi
fi
echo "type=$BUMP_TYPE" >> $GITHUB_OUTPUT
echo "Version bump type: $BUMP_TYPE"
- name: Bump version
id: version
run: |
BUMP_TYPE="${{ steps.bump-type.outputs.type }}"
# Bump version in Cargo.toml
cargo set-version --bump $BUMP_TYPE
# Get new version
NEW_VERSION=$(grep '^version = ' Cargo.toml | sed 's/version = "\(.*\)"/\1/')
TAG="v$NEW_VERSION"
echo "version=$NEW_VERSION" >> $GITHUB_OUTPUT
echo "tag=$TAG" >> $GITHUB_OUTPUT
echo "Bumped version to: $NEW_VERSION"
echo "Git tag will be: $TAG"
# Generate changelog
PREVIOUS_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
if [ -n "$PREVIOUS_TAG" ]; then
echo "Generating changelog from $PREVIOUS_TAG to HEAD"
CHANGELOG=$(git log --pretty=format:"- %s (%h)" $PREVIOUS_TAG..HEAD --no-merges | head -20)
else
echo "No previous tag found, generating changelog from recent commits"
CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges -10)
fi
# Save changelog to output (escape for GitHub Actions)
{
echo "changelog<<EOF"
echo "$CHANGELOG"
echo "EOF"
} >> $GITHUB_OUTPUT
- name: Update Cargo.lock
run: |
cargo check --quiet
echo "Cargo.lock updated"
- name: Commit version bump
run: |
git add Cargo.toml Cargo.lock
git commit -m "chore: bump version to ${{ steps.version.outputs.version }}"
git push origin main
- name: Create and push tag
run: |
git tag ${{ steps.version.outputs.tag }}
git push origin ${{ steps.version.outputs.tag }}
echo "Created and pushed tag: ${{ steps.version.outputs.tag }}"
- name: Create GitHub Release
if: github.event.inputs.skip_release != 'true'
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ steps.version.outputs.tag }}
release_name: Release ${{ steps.version.outputs.tag }}
body: |
## Changes in ${{ steps.version.outputs.tag }}
${{ steps.version.outputs.changelog }}
## Deployment Information
- **Environment**: Production
- **Build**: ${{ github.sha }}
- **Workflow**: [View workflow run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})
draft: false
prerelease: ${{ contains(steps.version.outputs.version, 'alpha') || contains(steps.version.outputs.version, 'beta') || contains(steps.version.outputs.version, 'rc') }}
build-web-ui:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '18'
cache: 'npm'
cache-dependency-path: 'web-ui/package-lock.json'
- name: Install dependencies
run: |
cd web-ui
npm ci
- name: Build web UI
run: |
cd web-ui
npm run build
- name: Prepare static files
run: |
mkdir -p static
cp -r web-ui/build/* static/
- name: Upload static files artifact
uses: actions/upload-artifact@v4
with:
name: static-files
path: static/
retention-days: 1
setup-azure-infrastructure:
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
outputs:
registry-name: ${{ steps.setup.outputs.registry-name || steps.existing-setup.outputs.registry-name }}
setup-required: ${{ steps.check.outputs.setup-required }}
steps:
- uses: actions/checkout@v4
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Check if infrastructure setup is required
id: check
run: |
# Check if resource group exists
if az group show --name ${{ env.RESOURCE_GROUP }} &> /dev/null; then
echo "Resource group exists"
# Check if ACR exists
REGISTRY_COUNT=$(az acr list --resource-group ${{ env.RESOURCE_GROUP }} --query "length(@)" --output tsv)
if [ "$REGISTRY_COUNT" -gt 0 ]; then
echo "setup-required=false" >> $GITHUB_OUTPUT
echo "Infrastructure already exists"
else
echo "setup-required=true" >> $GITHUB_OUTPUT
echo "ACR not found, setup required"
fi
else
echo "setup-required=true" >> $GITHUB_OUTPUT
echo "Resource group not found, setup required"
fi
# Force setup if requested
if [ "${{ github.event.inputs.force_setup }}" == "true" ]; then
echo "setup-required=true" >> $GITHUB_OUTPUT
echo "Force setup requested"
fi
- name: Register Azure Resource Providers
if: steps.check.outputs.setup-required == 'true'
run: |
echo "🔧 Registering Azure Resource Providers..."
PROVIDERS=(
"Microsoft.ContainerInstance"
"Microsoft.ContainerRegistry"
"Microsoft.Network"
"Microsoft.Resources"
)
for PROVIDER in "${PROVIDERS[@]}"; do
echo "Checking: $PROVIDER"
PROVIDER_STATE=$(az provider show --namespace "$PROVIDER" --query "registrationState" --output tsv 2>/dev/null || echo "NotRegistered")
if [ "$PROVIDER_STATE" != "Registered" ]; then
echo "🔄 Registering $PROVIDER..."
az provider register --namespace "$PROVIDER"
# Wait for registration with timeout
TIMEOUT=300 # 5 minutes
ELAPSED=0
while [ "$(az provider show --namespace "$PROVIDER" --query 'registrationState' --output tsv 2>/dev/null || echo 'NotRegistered')" != "Registered" ] && [ $ELAPSED -lt $TIMEOUT ]; do
echo " Waiting for $PROVIDER registration... (${ELAPSED}s)"
sleep 15
ELAPSED=$((ELAPSED + 15))
done
if [ $ELAPSED -ge $TIMEOUT ]; then
echo "⚠️ Warning: $PROVIDER registration timed out, but continuing..."
else
echo "✅ $PROVIDER registered successfully"
fi
else
echo "✅ $PROVIDER already registered"
fi
done
- name: Create Resource Group
if: steps.check.outputs.setup-required == 'true'
run: |
echo "🏗️ Creating resource group: ${{ env.RESOURCE_GROUP }}"
az group create --name ${{ env.RESOURCE_GROUP }} --location ${{ env.LOCATION }}
- name: Deploy Azure Infrastructure
id: setup
if: steps.check.outputs.setup-required == 'true'
run: |
echo "🚀 Deploying Azure infrastructure..."
az deployment group create \
--resource-group ${{ env.RESOURCE_GROUP }} \
--template-file infrastructure/azure-setup.bicep \
--parameters environment=prod deployContainer=false
# Get registry name
REGISTRY_NAME=$(az acr list --resource-group ${{ env.RESOURCE_GROUP }} --query "[0].name" --output tsv)
echo "registry-name=$REGISTRY_NAME" >> $GITHUB_OUTPUT
# Enable admin user
az acr update --name $REGISTRY_NAME --admin-enabled true
echo "✅ Infrastructure setup completed"
echo "📦 Registry: $REGISTRY_NAME.azurecr.io"
- name: Get existing registry name
id: existing-setup
if: steps.check.outputs.setup-required == 'false'
run: |
REGISTRY_NAME=$(az acr list --resource-group ${{ env.RESOURCE_GROUP }} --query "[0].name" --output tsv)
echo "registry-name=$REGISTRY_NAME" >> $GITHUB_OUTPUT
echo "📦 Using existing registry: $REGISTRY_NAME.azurecr.io"
build-and-push:
needs: [test, build-web-ui, setup-azure-infrastructure, version-and-release]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch'
outputs:
image-tag: ${{ steps.meta.outputs.tags }}
image-digest: ${{ steps.build.outputs.digest }}
registry-name: ${{ needs.setup-azure-infrastructure.outputs.registry-name }}
steps:
- uses: actions/checkout@v4
with:
ref: main # Ensure we get the updated version
- name: Download static files
uses: actions/download-artifact@v4
with:
name: static-files
path: static/
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Get ACR credentials
id: acr-creds
run: |
REGISTRY_NAME="${{ needs.setup-azure-infrastructure.outputs.registry-name }}"
ACR_USERNAME=$(az acr credential show --name $REGISTRY_NAME --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name $REGISTRY_NAME --query passwords[0].value --output tsv)
echo "::add-mask::$ACR_PASSWORD"
echo "username=$ACR_USERNAME" >> $GITHUB_OUTPUT
echo "password=$ACR_PASSWORD" >> $GITHUB_OUTPUT
- name: Login to Azure Container Registry
uses: docker/login-action@v3
with:
registry: ${{ needs.setup-azure-infrastructure.outputs.registry-name }}.azurecr.io
username: ${{ steps.acr-creds.outputs.username }}
password: ${{ steps.acr-creds.outputs.password }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ needs.setup-azure-infrastructure.outputs.registry-name }}.azurecr.io/${{ env.IMAGE_NAME }}
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=sha-,format=short
type=raw,value=latest,enable={{is_default_branch}}
type=raw,value=${{ needs.version-and-release.outputs.version }}
type=raw,value=${{ needs.version-and-release.outputs.tag }}
- name: Build and push Docker image
id: build
uses: docker/build-push-action@v5
with:
context: .
platforms: linux/amd64
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: |
${{ steps.meta.outputs.labels }}
org.opencontainers.image.version=${{ needs.version-and-release.outputs.version }}
org.opencontainers.image.revision=${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max
deploy-staging:
needs: [build-and-push, version-and-release]
runs-on: ubuntu-latest
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment: staging
steps:
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Extract short SHA
id: sha
run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Get ACR credentials
id: acr-creds
run: |
REGISTRY_NAME="${{ needs.build-and-push.outputs.registry-name }}"
ACR_USERNAME=$(az acr credential show --name $REGISTRY_NAME --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name $REGISTRY_NAME --query passwords[0].value --output tsv)
echo "::add-mask::$ACR_PASSWORD"
echo "username=$ACR_USERNAME" >> $GITHUB_OUTPUT
echo "password=$ACR_PASSWORD" >> $GITHUB_OUTPUT
- name: Deploy to Azure Container Instances (Staging)
uses: azure/aci-deploy@v1
with:
resource-group: ${{ env.RESOURCE_GROUP }}
dns-name-label: reframe-api-staging-${{ steps.sha.outputs.short }}
image: ${{ needs.build-and-push.outputs.registry-name }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ needs.version-and-release.outputs.version }}
name: ${{ env.CONTAINER_NAME }}-staging
location: ${{ env.LOCATION }}
cpu: 0.5
memory: 1
ports: '3000'
protocol: TCP
environment-variables: |
RUST_LOG=info
PORT=3000
APP_VERSION=${{ needs.version-and-release.outputs.version }}
registry-login-server: ${{ needs.build-and-push.outputs.registry-name }}.azurecr.io
registry-username: ${{ steps.acr-creds.outputs.username }}
registry-password: ${{ steps.acr-creds.outputs.password }}
- name: Test staging deployment
run: |
# Wait for container to be ready
sleep 30
# Get the FQDN
FQDN=$(az container show \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name ${{ env.CONTAINER_NAME }}-staging \
--query ipAddress.fqdn \
--output tsv)
echo "🧪 Testing staging deployment at: http://${FQDN}:3000"
echo "📦 Version: ${{ needs.version-and-release.outputs.version }}"
# Test the health endpoint
curl -f "http://${FQDN}:3000/health" || echo "Health check endpoint not available"
# Test the main API endpoint with sample data
echo "Testing main API endpoint..."
curl -X POST "http://${FQDN}:3000/reframe" \
-H "Content-Type: text/plain" \
-d "{1:F01BNPAFRPPXXX0000000000}{2:O1031234240101DEUTDEFFXXXX12345678952401011234N}{3:{103:EBA}}{4:
:20:FT21001234567890
:23B:CRED
:32A:240101USD1000,00
:50K:/1234567890
ACME CORPORATION
:52A:BNPAFRPPXXX
:57A:DEUTDEFFXXX
:59:/DE89370400440532013000
MUELLER GMBH
:70:PAYMENT FOR INVOICE 12345
:71A:OUR
-}" \
--max-time 30 \
-w "\nHTTP Status: %{http_code}\n" || echo "API test failed"
deploy-production:
needs: [build-and-push, deploy-staging, version-and-release]
runs-on: ubuntu-latest
if: (github.ref == 'refs/heads/main' && github.event_name == 'push') || (github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'production')
environment: production
outputs:
aci-fqdn: ${{ steps.get-fqdn.outputs.fqdn }}
steps:
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Extract short SHA
id: sha
run: echo "short=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT
- name: Get ACR credentials
id: acr-creds
run: |
REGISTRY_NAME="${{ needs.build-and-push.outputs.registry-name }}"
ACR_USERNAME=$(az acr credential show --name $REGISTRY_NAME --query username --output tsv)
ACR_PASSWORD=$(az acr credential show --name $REGISTRY_NAME --query passwords[0].value --output tsv)
echo "::add-mask::$ACR_PASSWORD"
echo "username=$ACR_USERNAME" >> $GITHUB_OUTPUT
echo "password=$ACR_PASSWORD" >> $GITHUB_OUTPUT
- name: Deploy to Azure Container Instances (Production)
uses: azure/aci-deploy@v1
with:
resource-group: ${{ env.RESOURCE_GROUP }}
dns-name-label: reframe-api-prod
image: ${{ needs.build-and-push.outputs.registry-name }}.azurecr.io/${{ env.IMAGE_NAME }}:${{ needs.version-and-release.outputs.version }}
name: ${{ env.CONTAINER_NAME }}-prod
location: ${{ env.LOCATION }}
cpu: 1
memory: 2
ports: '3000'
protocol: TCP
environment-variables: |
RUST_LOG=info
PORT=3000
APP_VERSION=${{ needs.version-and-release.outputs.version }}
registry-login-server: ${{ needs.build-and-push.outputs.registry-name }}.azurecr.io
registry-username: ${{ steps.acr-creds.outputs.username }}
registry-password: ${{ steps.acr-creds.outputs.password }}
- name: Get production endpoint
id: get-fqdn
run: |
FQDN=$(az container show \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name ${{ env.CONTAINER_NAME }}-prod \
--query ipAddress.fqdn \
--output tsv)
echo "fqdn=$FQDN" >> $GITHUB_OUTPUT
echo "🚀 Production deployment successful!"
echo "📦 Version: ${{ needs.version-and-release.outputs.version }}"
echo "🏷️ Tag: ${{ needs.version-and-release.outputs.tag }}"
echo "Web UI: http://${FQDN}:3000/"
echo "API endpoint: http://${FQDN}:3000/reframe"
echo "Health check: http://${FQDN}:3000/health"
- name: Test production deployment
run: |
FQDN="${{ steps.get-fqdn.outputs.fqdn }}"
# Wait for container to be ready
sleep 30
echo "🧪 Testing production deployment..."
echo "📦 Version: ${{ needs.version-and-release.outputs.version }}"
# Test health endpoint
echo "Testing health endpoint..."
curl -f "http://${FQDN}:3000/health" || echo "Health check failed"
# Test web UI (check if index.html is served)
echo "Testing web UI..."
curl -f "http://${FQDN}:3000/" | grep -q "Reframe" || echo "Web UI test failed"
# Test API endpoint
echo "Testing API endpoint..."
curl -X POST "http://${FQDN}:3000/reframe" \
-H "Content-Type: text/plain" \
-d "{1:F01BNPAFRPPXXX0000000000}{2:O1031234240101DEUTDEFFXXXX12345678952401011234N}{3:{103:EBA}}{4:
:20:FT21001234567890
:23B:CRED
:32A:240101USD1000,00
:50K:/1234567890
ACME CORPORATION
:52A:BNPAFRPPXXX
:57A:DEUTDEFFXXX
:59:/DE89370400440532013000
MUELLER GMBH
:70:PAYMENT FOR INVOICE 12345
:71A:OUR
-}" \
--max-time 30 \
-w "\nHTTP Status: %{http_code}\n" || echo "API test failed"
echo ""
echo "🎉 All tests completed!"
echo "✅ Version: ${{ needs.version-and-release.outputs.version }}"
echo "✅ Web UI: http://${FQDN}:3000/"
echo "✅ API: http://${FQDN}:3000/reframe"
- name: Update GitHub Release with deployment info
if: needs.version-and-release.outputs.tag != ''
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const fqdn = "${{ steps.get-fqdn.outputs.fqdn }}";
const version = "${{ needs.version-and-release.outputs.version }}";
const tag = "${{ needs.version-and-release.outputs.tag }}";
try {
// Get the release
const release = await github.rest.repos.getReleaseByTag({
owner: context.repo.owner,
repo: context.repo.repo,
tag: tag
});
// Update the release body with deployment information
const updatedBody = release.data.body + `
## 🚀 Deployment Completed
- **Production URL**: [http://${fqdn}:3000/](http://${fqdn}:3000/)
- **API Endpoint**: [http://${fqdn}:3000/reframe](http://${fqdn}:3000/reframe)
- **Health Check**: [http://${fqdn}:3000/health](http://${fqdn}:3000/health)
- **Deployed At**: ${new Date().toISOString()}
- **Git SHA**: ${context.sha}`;
await github.rest.repos.updateRelease({
owner: context.repo.owner,
repo: context.repo.repo,
release_id: release.data.id,
body: updatedBody
});
console.log(`Updated release ${tag} with deployment information`);
} catch (error) {
console.log(`Could not update release: ${error.message}`);
}
cleanup-staging:
needs: [deploy-production]
runs-on: ubuntu-latest
if: success()
steps:
- name: Login to Azure
uses: azure/login@v1
with:
creds: ${{ secrets.AZURE_CREDENTIALS }}
- name: Cleanup staging environment
run: |
az container delete \
--resource-group ${{ env.RESOURCE_GROUP }} \
--name ${{ env.CONTAINER_NAME }}-staging \
--yes || echo "Staging container not found or already deleted"