Skip to content

Run Integration Tests #117

Run Integration Tests

Run Integration Tests #117

Workflow file for this run

name: Run Integration Tests
on:
workflow_dispatch:
inputs:
fcli_version:
description: 'Fcli version (tag from GitHub releases)'
required: false
default: 'dev_v3.x'
products:
description: 'Products to test (comma-separated: fod, ssc)'
required: false
default: 'fod,ssc'
components:
description: 'Components to test (comma-separated: setup, ast-scan)'
required: false
default: 'setup,ast-scan'
source_dirs:
description: 'Source directories to test (comma-separated)'
required: false
default: 'node'
ci_systems:
description: 'CI systems to test (comma-separated: github:v2, github:v3, gitlab:v2, ado:v1)'
required: false
default: 'github:feat/fcli-ci,gitlab:v2,ado:v0'
os:
description: 'Operating systems to test (comma-separated: linux, windows, mac)'
required: false
default: 'linux,windows'
repo_tags:
description: 'Repository tags to test (comma-separated: public, private)'
required: false
default: 'public,private'
max_parallel:
description: 'Maximum number of parallel test jobs'
required: false
default: '2'
timeout_minutes:
description: 'Pipeline polling timeout in minutes'
required: false
default: '60'
concurrency:
group: fcli-ci-tests-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write # For syncing files to remote repos
actions: write # For triggering workflows in remote repos
jobs:
prepare-matrix:
name: Prepare Test Matrix
runs-on: ubuntu-latest
outputs:
matrix: ${{ steps.create-matrix.outputs.matrix }}
summary: ${{ steps.create-matrix.outputs.summary }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Create test matrix
id: create-matrix
run: |
set -x # Enable debug output
# Parse inputs
IFS=',' read -ra CI_SYSTEMS <<< "${{ github.event.inputs.ci_systems }}"
IFS=',' read -ra PRODUCTS <<< "${{ github.event.inputs.products }}"
IFS=',' read -ra COMPONENTS <<< "${{ github.event.inputs.components }}"
IFS=',' read -ra SOURCE_DIRS <<< "${{ github.event.inputs.source_dirs }}"
IFS=',' read -ra OS_LIST <<< "${{ github.event.inputs.os }}"
IFS=',' read -ra REPO_TAGS <<< "${{ github.event.inputs.repo_tags }}"
echo "Input parameters:"
echo " CI_SYSTEMS: ${CI_SYSTEMS[@]}"
echo " PRODUCTS: ${PRODUCTS[@]}"
echo " COMPONENTS: ${COMPONENTS[@]}"
echo " SOURCE_DIRS: ${SOURCE_DIRS[@]}"
echo " OS_LIST: ${OS_LIST[@]}"
echo " REPO_TAGS: ${REPO_TAGS[@]}"
# Read sources configuration
if [ -f "sources/config.json" ]; then
echo "Loading sources/config.json for build tool configuration"
sources_config=$(cat sources/config.json)
else
echo "WARNING: sources/config.json not found, build tool setup will be skipped"
sources_config='{"sources": {}}'
fi
# Build matrix with filtering
matrix_items=()
filtered_count=0
filtered_items=""
for ci_system in "${CI_SYSTEMS[@]}"; do
platform=$(echo "$ci_system" | cut -d: -f1)
version=$(echo "$ci_system" | cut -d: -f2)
echo "Processing CI system: $ci_system (platform=$platform, version=$version)"
# Check if config file exists
if [ ! -f "ci/$platform/config.json" ]; then
echo "ERROR: Config file ci/$platform/config.json not found"
continue
fi
# Read supported OS from config
supported_os=$(jq -r '.supportedOs[]' "ci/$platform/config.json" 2>/dev/null | tr '\n' ',')
echo " Supported OS for $platform: $supported_os"
# Loop through all repos for this platform
for repo in $(jq -c '.repos[]' "ci/$platform/config.json"); do
repo_tag=$(echo "$repo" | jq -r '.tag')
# Filter by repo_tags input
if ! printf '%s\n' "${REPO_TAGS[@]}" | grep -qx "$repo_tag"; then
echo " Filtering: skipping repo '$repo_tag' (not in repo_tags input)"
continue
fi
echo " Processing repo: $repo_tag"
for component in "${COMPONENTS[@]}"; do
echo " Processing component: $component"
# For setup component, no need to iterate products (product-agnostic)
if [ "$component" = "setup" ]; then
for os in "${OS_LIST[@]}"; do
if echo "$supported_os" | grep -q "$os"; then
echo " Adding: $ci_system / $repo_tag / $component / $os"
matrix_items+=("{\"ci_system\":\"$ci_system\",\"platform\":\"$platform\",\"version\":\"$version\",\"repo_tag\":\"$repo_tag\",\"product\":\"none\",\"component\":\"$component\",\"source_dir\":\"none\",\"os\":\"$os\",\"gitlab_image\":\"\"}")
else
echo " Filtering: $ci_system + $os (not supported)"
filtered_count=$((filtered_count + 1))
filtered_items="${filtered_items}- $ci_system / $repo_tag / none / $component / none / $os\n"
fi
done
else
# For ast-scan, iterate products and source_dirs
for product in "${PRODUCTS[@]}"; do
for source_dir in "${SOURCE_DIRS[@]}"; do
# Get GitLab image configuration for this source
gitlab_image=$(echo "$sources_config" | jq -r ".sources[\"$source_dir\"].gitlabImage // \"\"")
for os in "${OS_LIST[@]}"; do
if echo "$supported_os" | grep -q "$os"; then
echo " Adding: $ci_system / $repo_tag / $product / $component / $source_dir / $os"
matrix_items+=("{\"ci_system\":\"$ci_system\",\"platform\":\"$platform\",\"version\":\"$version\",\"repo_tag\":\"$repo_tag\",\"product\":\"$product\",\"component\":\"$component\",\"source_dir\":\"$source_dir\",\"os\":\"$os\",\"gitlab_image\":\"$gitlab_image\"}")
else
echo " Filtering: $ci_system + $os (not supported)"
filtered_count=$((filtered_count + 1))
filtered_items="${filtered_items}- $ci_system / $repo_tag / $product / $component / $source_dir / $os\n"
fi
done
done
done
fi
done
done
done
echo "Total matrix items: ${#matrix_items[@]}"
# Create JSON array
if [ ${#matrix_items[@]} -eq 0 ]; then
matrix_json="[]"
else
matrix_json=$(printf '%s\n' "${matrix_items[@]}" | jq -s -c .)
fi
echo "Matrix JSON: $matrix_json"
echo "matrix<<EOF" >> $GITHUB_OUTPUT
echo "$matrix_json" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Create summary
summary="Created ${#matrix_items[@]} test combinations."
if [ $filtered_count -gt 0 ]; then
summary="$summary Filtered out $filtered_count incompatible combinations:\n$filtered_items"
fi
echo "summary<<EOF" >> $GITHUB_OUTPUT
echo -e "$summary" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
- name: Display matrix summary
run: |
echo "## Test Matrix Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "${{ steps.create-matrix.outputs.summary }}" >> $GITHUB_STEP_SUMMARY
# Sync external CI repositories (GitLab, ADO) once before running tests
sync-external-repos:
name: Sync ${{ matrix.platform }} Repository
needs: prepare-matrix
runs-on: ubuntu-latest
strategy:
matrix:
platform: [github, gitlab, ado]
env:
# All potential secrets - scripts will dynamically select based on config.json
GH_PAT: ${{ secrets.GH_PAT }}
GITLAB_PUBLIC_TOKEN: ${{ secrets.GITLAB_PUBLIC_TOKEN }}
GITLAB_PRIVATE_TOKEN: ${{ secrets.GITLAB_PRIVATE_TOKEN }}
ADO_PAT: ${{ secrets.ADO_PAT }}
# Fortify test credentials
FCLI_FT_FOD_URL: ${{ vars.FCLI_FT_FOD_URL }}
FCLI_FT_SSC_URL: ${{ vars.FCLI_FT_SSC_URL }}
FCLI_FT_FOD_CLIENT_ID: ${{ secrets.FCLI_FT_FOD_CLIENT_ID }}
FCLI_FT_FOD_CLIENT_SECRET: ${{ secrets.FCLI_FT_FOD_CLIENT_SECRET }}
FCLI_FT_SSC_TOKEN: ${{ secrets.FCLI_FT_SSC_TOKEN }}
FCLI_FT_SC_SAST_TOKEN: ${{ secrets.FCLI_FT_SC_SAST_TOKEN }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Check if platform is needed
id: check
run: |
if echo '${{ needs.prepare-matrix.outputs.matrix }}' | grep -q '"platform":"${{ matrix.platform }}"'; then
echo "needed=true" >> $GITHUB_OUTPUT
else
echo "needed=false" >> $GITHUB_OUTPUT
echo "Skipping ${{ matrix.platform }} - not in test matrix"
fi
- name: Sync repository
if: steps.check.outputs.needed == 'true'
run: |
set -e
# Read platform config
PLATFORM="${{ matrix.platform }}"
# Loop through all repos for this platform
for repo in $(jq -c '.repos[]' "ci/$PLATFORM/config.json"); do
REPO_TAG=$(echo "$repo" | jq -r '.tag')
REPO_URL=$(echo "$repo" | jq -r '.url')
PAT_SECRET=$(echo "$repo" | jq -r '.pat_secret')
PIPELINE_FILE=$([ "$PLATFORM" = "gitlab" ] && echo ".gitlab-ci.yml" || echo "azure-pipelines.yml")
echo "===================================="
echo "Syncing $PLATFORM repository '$REPO_TAG': $REPO_URL"
echo "Using PAT secret: $PAT_SECRET"
echo "===================================="
# Get the PAT value using variable indirection
PAT_VALUE="${!PAT_SECRET}"
# Set up authentication based on platform
if [ "$PLATFORM" = "gitlab" ]; then
AUTH_URL="https://oauth2:$PAT_VALUE@${REPO_URL#https://}"
elif [ "$PLATFORM" = "ado" ]; then
AUTH_URL="https://anything:$PAT_VALUE@${REPO_URL#https://}"
else
# GitHub
AUTH_URL="https://x-access-token:$PAT_VALUE@${REPO_URL#https://}"
fi
# Clone repo to temp directory
temp_dir=$(mktemp -d)
git clone "$AUTH_URL" "$temp_dir"
cd "$temp_dir"
# Check if main branch exists
echo "Checking for pipeline or source changes..."
if git ls-remote --heads origin main | grep -q main; then
git fetch origin main
else
echo "Repository is empty or main branch doesn't exist yet"
git checkout -b main
fi
# Copy all CI files for this platform to root
echo "Syncing ci/$PLATFORM files to root..."
cp -r "$GITHUB_WORKSPACE/ci/$PLATFORM/"* .
cp -r "$GITHUB_WORKSPACE/ci/$PLATFORM/".* . 2>/dev/null || true
# Copy sources
rm -rf sources
cp -r "$GITHUB_WORKSPACE/sources" .
# Check if anything changed (new files or modified files)
if ! git diff --quiet || [ -n "$(git ls-files --others --exclude-standard)" ]; then
echo "Changes detected, pushing to $PLATFORM '$REPO_TAG'..."
git config user.name "fcli-ci-test-runner"
git config user.email "fcli-ci-test-runner@fortify.github.io"
git add .
git commit -m "CI Test Run ${{ github.run_id }} - Sync from fcli-ci-test-runner@${{ github.sha }}"
git push --force origin HEAD:main
else
echo "No changes detected, skipping git push"
fi
cd "$GITHUB_WORKSPACE"
rm -rf "$temp_dir"
echo "✓ Repository '$REPO_TAG' synced successfully"
done
- name: Sync GitLab secrets and variables
if: steps.check.outputs.needed == 'true' && matrix.platform == 'gitlab'
run: |
set -e
# Loop through all repos for GitLab
for repo in $(jq -c '.repos[]' "ci/gitlab/config.json"); do
REPO_TAG=$(echo "$repo" | jq -r '.tag')
GITLAB_REPO_URL=$(echo "$repo" | jq -r '.url')
PAT_SECRET=$(echo "$repo" | jq -r '.pat_secret')
GITLAB_PROJECT_ID=$(echo "$GITLAB_REPO_URL" | sed 's|https://gitlab.com/||' | sed 's|/|%2F|g')
# Get the PAT value using variable indirection
GITLAB_TOKEN="${!PAT_SECRET}"
echo "===================================="
echo "Syncing variables/secrets to GitLab repo '$REPO_TAG'..."
echo "Using PAT secret: $PAT_SECRET"
echo "===================================="
# Sync regular variables (non-masked)
for var_name in $(jq -r '.variableNames[]' ci/gitlab/config.json); do
var_value="${!var_name}"
if [ -n "$var_value" ]; then
echo "Updating variable: $var_name"
curl -s -X DELETE \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/variables/$var_name" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" 2>/dev/null || true
curl -f -X POST \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/variables" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"key\": \"$var_name\",
\"value\": \"$var_value\",
\"masked\": false,
\"protected\": false
}"
fi
done
# Sync secrets (masked)
for secret_name in $(jq -r '.secretNames[]' ci/gitlab/config.json); do
secret_value="${!secret_name}"
if [ -n "$secret_value" ]; then
echo "Updating secret: $secret_name"
curl -s -X DELETE \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/variables/$secret_name" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" 2>/dev/null || true
curl -f -X POST \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/variables" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" \
-H "Content-Type: application/json" \
-d "{
\"key\": \"$secret_name\",
\"value\": \"$secret_value\",
\"masked\": true,
\"protected\": false
}"
fi
done
echo "✓ GitLab secrets and variables synced to '$REPO_TAG' successfully"
done
- name: Sync ADO secrets
if: steps.check.outputs.needed == 'true' && matrix.platform == 'ado'
run: |
set -e
# Loop through all repos for ADO
for repo in $(jq -c '.repos[]' "ci/ado/config.json"); do
REPO_TAG=$(echo "$repo" | jq -r '.tag')
ADO_REPO_URL=$(echo "$repo" | jq -r '.url')
PAT_SECRET=$(echo "$repo" | jq -r '.pat_secret')
ADO_ORG=$(echo "$ADO_REPO_URL" | sed 's|https://dev.azure.com/||' | cut -d'/' -f1)
ADO_PROJ=$(echo "$ADO_REPO_URL" | sed 's|https://dev.azure.com/||' | cut -d'/' -f2)
# Get the PAT value using variable indirection
ADO_PAT="${!PAT_SECRET}"
echo "===================================="
echo "Syncing secrets to Azure DevOps repo '$REPO_TAG' ($ADO_ORG/$ADO_PROJ)..."
echo "Using PAT secret: $PAT_SECRET"
echo "===================================="
# Create auth header
AUTH_HEADER="Authorization: Basic $(printf ":%s" "$ADO_PAT" | base64 -w 0)"
# Variable group name (consistent across all repos)
VG_NAME="fcli-ci-test"
# Get existing variable group
echo "Fetching $VG_NAME variable group..."
vg_response=$(curl -s \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/distributedtask/variablegroups?api-version=7.1-preview.2" \
-H "$AUTH_HEADER")
vg_id=$(echo "$vg_response" | jq -r ".value[] | select(.name == \"$VG_NAME\") | .id")
if [ -z "$vg_id" ] || [ "$vg_id" = "null" ]; then
echo "Error: Variable group '$VG_NAME' not found. Please create it manually in Azure DevOps."
exit 1
fi
echo "Found variable group ID: $vg_id"
# Get full variable group details to preserve project references
vg_details=$(curl -s \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/distributedtask/variablegroups/$vg_id?api-version=7.1-preview.2" \
-H "$AUTH_HEADER")
# Extract existing project references
project_refs=$(echo "$vg_details" | jq -c '.variableGroupProjectReferences // []')
# Build variables JSON
variables_json='{'
first=true
# Add regular variables (URLs)
for var_name in $(jq -r '.variableNames[]' ci/ado/config.json); do
var_value="${!var_name}"
if [ -n "$var_value" ]; then
if [ "$first" = false ]; then
variables_json="$variables_json,"
fi
variables_json="$variables_json\"$var_name\":{\"value\":\"$var_value\",\"isSecret\":false}"
first=false
fi
done
# Add secrets
for secret_name in $(jq -r '.secretNames[]' ci/ado/config.json); do
secret_value="${!secret_name}"
if [ -n "$secret_value" ]; then
if [ "$first" = false ]; then
variables_json="$variables_json,"
fi
variables_json="$variables_json\"$secret_name\":{\"value\":\"$secret_value\",\"isSecret\":true}"
first=false
fi
done
variables_json="$variables_json}"
# Update variable group
echo "Updating variable group with secrets..."
update_response=$(curl -s -X PUT \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/distributedtask/variablegroups/$vg_id?api-version=7.1-preview.2" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "{
\"id\": $vg_id,
\"name\": \"$VG_NAME\",
\"type\": \"Vsts\",
\"variables\": $variables_json,
\"variableGroupProjectReferences\": $project_refs
}")
# Check if update was successful
updated_id=$(echo "$update_response" | jq -r '.id // empty')
if [ -z "$updated_id" ]; then
echo "Error: Failed to update variable group"
echo "Response: $update_response"
exit 1
fi
echo "✓ Azure DevOps secrets synced to '$REPO_TAG' successfully"
done
- name: Sync GitHub secrets and variables
if: steps.check.outputs.needed == 'true' && matrix.platform == 'github'
run: |
set -e
# Loop through all repos for GitHub
for repo in $(jq -c '.repos[]' "ci/github/config.json"); do
REPO_TAG=$(echo "$repo" | jq -r '.tag')
GITHUB_REPO_URL=$(echo "$repo" | jq -r '.url')
PAT_SECRET=$(echo "$repo" | jq -r '.pat_secret')
GITHUB_REPO=$(echo "$GITHUB_REPO_URL" | sed 's|https://github.com/||')
# Get the PAT value using variable indirection
GH_TOKEN="${!PAT_SECRET}"
export GH_TOKEN
echo "===================================="
echo "Syncing secrets/variables to GitHub repo '$REPO_TAG'..."
echo "Using PAT secret: $PAT_SECRET"
echo "===================================="
# Sync regular variables (not encrypted)
for var_name in $(jq -r '.variableNames[]' ci/github/config.json); do
var_value="${!var_name}"
if [ -n "$var_value" ]; then
echo "Updating variable: $var_name"
# Delete if exists, then create
gh variable delete "$var_name" --repo "$GITHUB_REPO" 2>/dev/null || true
gh variable set "$var_name" --body "$var_value" --repo "$GITHUB_REPO"
fi
done
# Sync secrets (using gh CLI which handles encryption)
for secret_name in $(jq -r '.secretNames[]' ci/github/config.json); do
secret_value="${!secret_name}"
if [ -n "$secret_value" ]; then
echo "Updating secret: $secret_name"
# gh secret set handles encryption automatically
echo "$secret_value" | gh secret set "$secret_name" --repo "$GITHUB_REPO"
fi
done
echo "✓ GitHub secrets and variables synced to '$REPO_TAG' successfully"
done
test:
name: Test ${{ matrix.ci_system }} / ${{ matrix.repo_tag }} / ${{ matrix.component }} / ${{ matrix.product }} / fcli ${{ github.event.inputs.fcli_version }} / ${{ matrix.source_dir }} / ${{ matrix.os }}
needs: [prepare-matrix, sync-external-repos]
runs-on: ubuntu-latest
if: |
!cancelled() &&
needs.prepare-matrix.outputs.matrix != '[]' &&
(needs.sync-external-repos.result == 'success' || needs.sync-external-repos.result == 'skipped')
strategy:
fail-fast: false
max-parallel: ${{ fromJSON(github.event.inputs.max_parallel) }}
matrix:
include: ${{ fromJSON(needs.prepare-matrix.outputs.matrix) }}
env:
# All potential secrets - scripts will dynamically select based on config.json
GH_PAT: ${{ secrets.GH_PAT }}
GITLAB_PUBLIC_TOKEN: ${{ secrets.GITLAB_PUBLIC_TOKEN }}
GITLAB_PRIVATE_TOKEN: ${{ secrets.GITLAB_PRIVATE_TOKEN }}
GITLAB_PUBLIC_TRIGGER_TOKEN: ${{ secrets.GITLAB_PUBLIC_TRIGGER_TOKEN }}
GITLAB_PRIVATE_TRIGGER_TOKEN: ${{ secrets.GITLAB_PRIVATE_TRIGGER_TOKEN }}
ADO_PAT: ${{ secrets.ADO_PAT }}
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Generate Fortify release/appversion name
id: fortify-release
run: |
# Generate: fcli/ci-test/<platform>/<repo_tag>/<version>/<os>/<source_dir>:current
# Skip source_dir if component is setup (uses 'none')
if [ "${{ matrix.component }}" = "setup" ]; then
release="fcli/ci-test/${{ matrix.platform }}/${{ matrix.repo_tag }}/${{ matrix.version }}/${{ matrix.os }}:current"
else
release="fcli/ci-test/${{ matrix.platform }}/${{ matrix.repo_tag }}/${{ matrix.version }}/${{ matrix.os }}/${{ matrix.source_dir }}:current"
fi
echo "name=$release" >> $GITHUB_OUTPUT
- name: Generate pipeline name
id: pipeline-name
run: |
# Generate consistent name: fcli version / version / component / product / os / source_dir
# For setup component, use 'none' for product
if [ "${{ matrix.component }}" = "setup" ]; then
name="fcli ${{ github.event.inputs.fcli_version }} / ${{ matrix.version }} / ${{ matrix.component }} / none / ${{ matrix.os }} / ${{ matrix.source_dir }}"
else
name="fcli ${{ github.event.inputs.fcli_version }} / ${{ matrix.version }} / ${{ matrix.component }} / ${{ matrix.product }} / ${{ matrix.os }} / ${{ matrix.source_dir }}"
fi
echo "name=$name" >> $GITHUB_OUTPUT
- name: Check for cancellation
id: check-cancelled
run: |
# Check if this workflow run has been cancelled
run_state=$(gh api \
"repos/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--jq '.status + ":" + (.conclusion // "")')
run_status=$(echo "$run_state" | cut -d: -f1)
run_conclusion=$(echo "$run_state" | cut -d: -f2)
if [ "$run_conclusion" = "cancelled" ]; then
echo "Workflow run has been cancelled, stopping job"
exit 1
fi
env:
GH_TOKEN: ${{ github.token }}
- name: Trigger GitHub test
id: github-trigger
if: matrix.platform == 'github'
run: |
set -e
run_id=""
GITHUB_REPO=""
# Store parent workflow token for cancellation checks
PARENT_GH_TOKEN="${{ github.token }}"
cleanup_on_cancel() {
echo "✗ Received termination signal, cleaning up..."
if [ -n "$run_id" ] && [ -n "$GITHUB_REPO" ]; then
echo "Cancelling downstream GitHub workflow run $run_id in $GITHUB_REPO"
gh run cancel "$run_id" --repo "$GITHUB_REPO" || true
fi
exit 143
}
trap cleanup_on_cancel TERM INT
check_parent_cancelled() {
parent_run_state=$(GH_TOKEN="$PARENT_GH_TOKEN" gh api \
"repos/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--jq '.status + ":" + (.conclusion // "")' 2>/dev/null || echo "unknown:unknown")
parent_run_status=$(echo "$parent_run_state" | cut -d: -f1)
parent_run_conclusion=$(echo "$parent_run_state" | cut -d: -f2)
echo "[DEBUG] Parent workflow status: $parent_run_status, conclusion: ${parent_run_conclusion:-none}"
if [ "$parent_run_conclusion" = "cancelled" ]; then
echo "✗ Parent workflow run has been cancelled, stopping test in repo '$REPO_TAG'"
echo "::warning::test cancelled: ${{ steps.pipeline-name.outputs.name }} - pipeline: $run_url"
exit 1
fi
}
# Get repo info from matrix
REPO_TAG="${{ matrix.repo_tag }}"
repo=$(jq -c ".repos[] | select(.tag == \"$REPO_TAG\")" "ci/github/config.json")
GITHUB_REPO_URL=$(echo "$repo" | jq -r '.url')
PAT_SECRET=$(echo "$repo" | jq -r '.pat_secret')
GITHUB_REPO=$(echo "$GITHUB_REPO_URL" | sed 's|https://github.com/||')
# Get the PAT value using variable indirection and export for gh CLI
GH_TOKEN="${!PAT_SECRET}"
export GH_TOKEN
echo "===================================="
echo "Triggering GitHub test in repo '$REPO_TAG' ($GITHUB_REPO)"
echo "Using PAT secret: $PAT_SECRET"
echo "===================================="
# Resolve runs-on label from repository config, fallback to legacy defaults
RUNS_ON=$(echo "$repo" | jq -r --arg os "${{ matrix.os }}" '.runsOn[$os] // empty')
if [ -z "$RUNS_ON" ] || [ "$RUNS_ON" = "null" ]; then
case "${{ matrix.os }}" in
linux) RUNS_ON="ubuntu-latest" ;;
windows) RUNS_ON="windows-latest" ;;
mac) RUNS_ON="macos-latest" ;;
*) RUNS_ON="ubuntu-latest" ;;
esac
fi
echo "Using runner label: $RUNS_ON"
# Trigger workflow and capture the time
trigger_time=$(date -u +%s)
gh workflow run test-pipeline.yml \
--repo "$GITHUB_REPO" \
-f pipeline_name="${{ steps.pipeline-name.outputs.name }}" \
-f version=${{ matrix.version }} \
-f fcli_version=${{ github.event.inputs.fcli_version }} \
-f product=${{ matrix.product }} \
-f component=${{ matrix.component }} \
-f source_dir=sources/${{ matrix.source_dir }} \
-f os=${{ matrix.os }} \
-f fortify_release=${{ steps.fortify-release.outputs.name }} \
-f runs_on="$RUNS_ON"
echo "Triggered GitHub workflow in '$REPO_TAG', waiting for run to start..."
pipeline_name="${{ steps.pipeline-name.outputs.name }}"
echo "Looking for run with name: $pipeline_name"
# Retry logic to find the workflow run (may take time to appear in API)
max_attempts=12
attempt=0
run_id=""
while [ $attempt -lt $max_attempts ] && [ -z "$run_id" ]; do
attempt=$((attempt + 1))
sleep 5
echo "Attempt $attempt/$max_attempts: Searching for workflow run..."
run_id=$(gh run list \
--repo "$GITHUB_REPO" \
--workflow=test-pipeline.yml \
--limit=30 \
--json databaseId,createdAt,displayTitle,status | \
jq --arg name "$pipeline_name" --arg trigger_time "$trigger_time" -r \
'[.[] | select(.createdAt | fromdateiso8601 > ($trigger_time | tonumber)) | select(.displayTitle == $name)] | .[0].databaseId')
if [ -n "$run_id" ] && [ "$run_id" != "null" ]; then
echo "Found workflow run ID: $run_id"
break
fi
run_id="" # Reset if null or empty
if [ $attempt -lt $max_attempts ]; then
echo "Run not found yet, retrying in 5 seconds..."
fi
done
if [ -z "$run_id" ]; then
echo "Error: Could not find triggered workflow run with displayTitle='$pipeline_name' after $max_attempts attempts"
echo "This may indicate the workflow was not triggered successfully or has a different display name"
exit 1
fi
run_url="https://github.com/$GITHUB_REPO/actions/runs/$run_id"
echo "Workflow URL: $run_url"
echo "Polling for completion (timeout: ${{ github.event.inputs.timeout_minutes }} minutes)..."
# Poll for completion
timeout=$((60 * ${{ github.event.inputs.timeout_minutes}}))
elapsed=0
poll_interval=30
check_interval=5
test_passed=false
while [ $elapsed -lt $timeout ]; do
check_parent_cancelled
status=$(gh run view $run_id --repo "$GITHUB_REPO" --json status,conclusion --jq '.status + ":" + .conclusion')
run_status=$(echo $status | cut -d: -f1)
run_conclusion=$(echo $status | cut -d: -f2)
echo "[$elapsed s] Status: $run_status, Conclusion: $run_conclusion"
if [ "$run_status" = "completed" ]; then
if [ "$run_conclusion" = "success" ]; then
echo "✓ Test passed in repo '$REPO_TAG'!"
echo "::notice::test success: ${{ steps.pipeline-name.outputs.name }} - pipeline: $run_url"
test_passed=true
else
echo "✗ Test failed in repo '$REPO_TAG' with conclusion: $run_conclusion"
echo "::error::test failure: ${{ steps.pipeline-name.outputs.name }} - pipeline: $run_url"
gh run view $run_id --repo "$GITHUB_REPO" --log-failed || true
test_passed=false
fi
break
fi
# Sleep in small chunks to check for cancellation more frequently
sleep_remaining=$poll_interval
while [ $sleep_remaining -gt 0 ] && [ $elapsed -lt $timeout ]; do
sleep $check_interval
elapsed=$((elapsed + check_interval))
sleep_remaining=$((sleep_remaining - check_interval))
# Check for cancellation during sleep
check_parent_cancelled
done
done
if [ $elapsed -ge $timeout ]; then
echo "✗ Test timed out after $timeout seconds in repo '$REPO_TAG'"
echo "::error::test failure: ${{ steps.pipeline-name.outputs.name }} - pipeline: $run_url (timeout)"
test_passed=false
fi
# Fail the job if test failed
if [ "$test_passed" != "true" ]; then
echo "ERROR: Test failed"
exit 1
fi
- name: Trigger GitLab test
id: gitlab-trigger
if: matrix.platform == 'gitlab'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -e
pipeline_id=""
GITLAB_PROJECT_ID=""
GITLAB_TOKEN=""
# Store parent workflow token for cancellation checks
PARENT_GH_TOKEN="${{ github.token }}"
cleanup_on_cancel() {
echo "✗ Received termination signal, cleaning up..."
if [ -n "$pipeline_id" ] && [ -n "$GITLAB_PROJECT_ID" ] && [ -n "$GITLAB_TOKEN" ]; then
echo "Cancelling downstream GitLab pipeline $pipeline_id in project $GITLAB_PROJECT_ID"
curl -s -X POST \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/pipelines/$pipeline_id/cancel" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" >/dev/null || true
fi
exit 143
}
trap cleanup_on_cancel TERM INT
check_parent_cancelled() {
parent_run_state=$(GH_TOKEN="$PARENT_GH_TOKEN" gh api \
"repos/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--jq '.status + ":" + (.conclusion // "")' 2>/dev/null || echo "unknown:unknown")
parent_run_status=$(echo "$parent_run_state" | cut -d: -f1)
parent_run_conclusion=$(echo "$parent_run_state" | cut -d: -f2)
echo "[DEBUG] Parent workflow status: $parent_run_status, conclusion: ${parent_run_conclusion:-none}"
if [ "$parent_run_conclusion" = "cancelled" ]; then
echo "✗ Parent workflow run has been cancelled, stopping test in repo '$REPO_TAG'"
echo "::warning::test cancelled: ${{ steps.pipeline-name.outputs.name }} - pipeline: $pipeline_url"
exit 1
fi
}
# Get repo info from matrix
REPO_TAG="${{ matrix.repo_tag }}"
repo=$(jq -c ".repos[] | select(.tag == \"$REPO_TAG\")" "ci/gitlab/config.json")
GITLAB_REPO_URL=$(echo "$repo" | jq -r '.url')
PAT_SECRET=$(echo "$repo" | jq -r '.pat_secret')
TRIGGER_SECRET=$(echo "$repo" | jq -r '.trigger_secret')
GITLAB_PROJECT_ID=$(echo "$GITLAB_REPO_URL" | sed 's|https://gitlab.com/||' | sed 's|/|%2F|g')
# Get the PAT and trigger token values using variable indirection
GITLAB_TOKEN="${!PAT_SECRET}"
GITLAB_TRIGGER_TOKEN="${!TRIGGER_SECRET}"
# Validate tokens
if [ -z "$GITLAB_TOKEN" ]; then
echo "ERROR: PAT token is empty (secret: $PAT_SECRET)"
exit 1
fi
if [ -z "$GITLAB_TRIGGER_TOKEN" ]; then
echo "ERROR: Trigger token is empty (secret: $TRIGGER_SECRET)"
exit 1
fi
echo "===================================="
echo "Triggering GitLab pipeline in repo '$REPO_TAG'"
echo "Using PAT secret: $PAT_SECRET"
echo "Using trigger secret: $TRIGGER_SECRET"
echo "Project ID: $GITLAB_PROJECT_ID"
echo "===================================="
echo "Triggering GitLab pipeline: ${{ steps.pipeline-name.outputs.name }}"
pipeline_response=$(curl -s -w "\nHTTP_CODE:%{http_code}" -X POST \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/trigger/pipeline" \
-F "token=$GITLAB_TRIGGER_TOKEN" \
-F "ref=main" \
-F "variables[PIPELINE_NAME]=${{ steps.pipeline-name.outputs.name }}" \
-F "variables[VERSION]=${{ matrix.version }}" \
-F "variables[FCLI_VERSION]=${{ github.event.inputs.fcli_version }}" \
-F "variables[PRODUCT]=${{ matrix.product }}" \
-F "variables[COMPONENT]=${{ matrix.component }}" \
-F "variables[SOURCE_DIR]=sources/${{ matrix.source_dir }}" \
-F "variables[OS]=${{ matrix.os }}" \
-F "variables[FORTIFY_RELEASE]=${{ steps.fortify-release.outputs.name }}" \
-F "variables[GITLAB_IMAGE]=${{ matrix.gitlab_image }}")
http_code=$(echo "$pipeline_response" | grep -o "HTTP_CODE:[0-9]*" | cut -d: -f2)
response_body=$(echo "$pipeline_response" | sed 's/HTTP_CODE:[0-9]*$//')
if [ "$http_code" != "201" ]; then
echo "ERROR: GitLab API returned HTTP $http_code"
echo "Response: $response_body"
exit 1
fi
pipeline_id=$(echo "$response_body" | jq -r '.id')
pipeline_url="$GITLAB_REPO_URL/-/pipelines/$pipeline_id"
echo "Pipeline ID: $pipeline_id"
echo "Pipeline URL: $pipeline_url"
echo "Polling for completion (timeout: ${{ github.event.inputs.timeout_minutes }} minutes)..."
timeout=$((60 * ${{ github.event.inputs.timeout_minutes }}))
elapsed=0
poll_interval=30
check_interval=5
test_passed=false
while [ $elapsed -lt $timeout ]; do
check_parent_cancelled
pipeline_status=$(curl -f -s \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/pipelines/$pipeline_id" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" | jq -r '.status')
echo "[$elapsed s] Status: $pipeline_status"
case "$pipeline_status" in
success)
echo "✓ Test passed in repo '$REPO_TAG'!"
echo "::notice::test success: ${{ steps.pipeline-name.outputs.name }} - pipeline: $pipeline_url"
test_passed=true
break
;;
failed|canceled|skipped)
echo "✗ Test failed in repo '$REPO_TAG' with status: $pipeline_status"
echo "::error::test failure: ${{ steps.pipeline-name.outputs.name }} - pipeline: $pipeline_url"
curl -f -s \
"https://gitlab.com/api/v4/projects/$GITLAB_PROJECT_ID/pipelines/$pipeline_id/jobs" \
-H "PRIVATE-TOKEN: $GITLAB_TOKEN" | jq '.[] | select(.status == "failed") | {name, stage, status}' || true
test_passed=false
break
;;
running|pending|created|waiting_for_resource|preparing)
# Continue polling
;;
*)
echo "Unknown status: $pipeline_status"
test_passed=false
break
;;
esac
# Sleep in small chunks to check for cancellation more frequently
sleep_remaining=$poll_interval
while [ $sleep_remaining -gt 0 ] && [ $elapsed -lt $timeout ]; do
sleep $check_interval
elapsed=$((elapsed + check_interval))
sleep_remaining=$((sleep_remaining - check_interval))
# Check for cancellation during sleep
check_parent_cancelled
done
done
if [ $elapsed -ge $timeout ]; then
echo "✗ Test timed out after $timeout seconds in repo '$REPO_TAG'"
echo "::error::test failure: ${{ steps.pipeline-name.outputs.name }} - pipeline: $pipeline_url (timeout)"
test_passed=false
fi
# Fail the job if test failed
if [ "$test_passed" != "true" ]; then
echo "ERROR: Test failed"
exit 1
fi
- name: Trigger ADO test
id: ado-trigger
if: matrix.platform == 'ado'
env:
GH_TOKEN: ${{ github.token }}
run: |
set -e
build_id=""
ADO_ORG=""
ADO_PROJ=""
AUTH_HEADER=""
# Store parent workflow token for cancellation checks
PARENT_GH_TOKEN="${{ github.token }}"
cleanup_on_cancel() {
echo "✗ Received termination signal, cleaning up..."
if [ -n "$build_id" ] && [ -n "$ADO_ORG" ] && [ -n "$ADO_PROJ" ] && [ -n "$AUTH_HEADER" ]; then
echo "Cancelling downstream ADO build $build_id in $ADO_ORG/$ADO_PROJ"
curl -s -X PATCH \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/build/builds/$build_id?api-version=7.1-preview.7" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d '{"status":"cancelling"}' >/dev/null || true
fi
exit 143
}
trap cleanup_on_cancel TERM INT
check_parent_cancelled() {
parent_run_state=$(GH_TOKEN="$PARENT_GH_TOKEN" gh api \
"repos/${{ github.repository }}/actions/runs/${{ github.run_id }}" \
--jq '.status + ":" + (.conclusion // "")' 2>/dev/null || echo "unknown:unknown")
parent_run_status=$(echo "$parent_run_state" | cut -d: -f1)
parent_run_conclusion=$(echo "$parent_run_state" | cut -d: -f2)
echo "[DEBUG] Parent workflow status: $parent_run_status, conclusion: ${parent_run_conclusion:-none}"
if [ "$parent_run_conclusion" = "cancelled" ]; then
echo "✗ Parent workflow run has been cancelled, stopping test in repo '$REPO_TAG'"
echo "::warning::test cancelled: ${{ steps.pipeline-name.outputs.name }} - pipeline: $build_url"
exit 1
fi
}
# Get repo info from matrix
REPO_TAG="${{ matrix.repo_tag }}"
repo=$(jq -c ".repos[] | select(.tag == \"$REPO_TAG\")" "ci/ado/config.json")
ADO_REPO_URL=$(echo "$repo" | jq -r '.url')
PAT_SECRET=$(echo "$repo" | jq -r '.pat_secret')
ADO_ORG=$(echo "$ADO_REPO_URL" | sed 's|https://dev.azure.com/||' | cut -d'/' -f1)
ADO_PROJ=$(echo "$ADO_REPO_URL" | sed 's|https://dev.azure.com/||' | cut -d'/' -f2)
ADO_REPO=$(echo "$ADO_REPO_URL" | sed 's|.*/||')
# Get the PAT value using variable indirection
ADO_PAT="${!PAT_SECRET}"
echo "===================================="
echo "Triggering Azure DevOps pipeline in repo '$REPO_TAG' ($ADO_ORG/$ADO_PROJ)"
echo "Using PAT secret: $PAT_SECRET"
echo "===================================="
# Create auth header
AUTH_HEADER="Authorization: Basic $(printf ":%s" "$ADO_PAT" | base64 -w 0)"
# Find the pipeline ID by querying for pipelines with path azure-pipelines.yml
echo "Looking up pipeline ID..."
pipelines_response=$(curl -s \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/pipelines?api-version=7.1-preview.1" \
-H "$AUTH_HEADER")
pipeline_id=$(echo "$pipelines_response" | jq -r '.value[] | select(.folder == "\\ci\\ado" or .folder == "/ci/ado" or (.configuration.path // "" | contains("azure-pipelines.yml"))) | .id' | head -n1)
# If not found by folder/path, try finding by name or just get the first pipeline
if [ -z "$pipeline_id" ] || [ "$pipeline_id" = "null" ]; then
echo "Pipeline not found by path, trying first available pipeline..."
pipeline_id=$(echo "$pipelines_response" | jq -r '.value[0].id')
fi
if [ -z "$pipeline_id" ] || [ "$pipeline_id" = "null" ]; then
echo "Error: Could not find pipeline ID"
echo "Available pipelines:"
echo "$pipelines_response" | jq '.value[] | {id, name, folder, path: .configuration.path}'
exit 1
fi
echo "Using pipeline ID: $pipeline_id"
# Debug: Show what we're about to send
echo "=== Pipeline Trigger Parameters ==="
echo "PIPELINE_NAME: ${{ steps.pipeline-name.outputs.name }}"
echo "VERSION: ${{ matrix.version }}"
echo "FCLI_VERSION: ${{ github.event.inputs.fcli_version }}"
echo "PRODUCT: ${{ matrix.product }}"
echo "COMPONENT: ${{ matrix.component }}"
echo "SOURCE_DIR: sources/${{ matrix.source_dir }}"
echo "OS: ${{ matrix.os }}"
echo "FORTIFY_RELEASE: ${{ steps.fortify-release.outputs.name }}"
echo "===================================="
build_response=$(curl -s -X POST \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/pipelines/$pipeline_id/runs?api-version=7.1-preview.1" \
-H "$AUTH_HEADER" \
-H "Content-Type: application/json" \
-d "{
\"resources\": {
\"repositories\": {
\"self\": {
\"refName\": \"refs/heads/main\"
}
}
},
\"templateParameters\": {
\"PIPELINE_NAME\": \"${{ steps.pipeline-name.outputs.name }}\",
\"VERSION\": \"${{ matrix.version }}\",
\"FCLI_VERSION\": \"${{ github.event.inputs.fcli_version }}\",
\"PRODUCT\": \"${{ matrix.product }}\",
\"COMPONENT\": \"${{ matrix.component }}\",
\"SOURCE_DIR\": \"sources/${{ matrix.source_dir }}\",
\"OS\": \"${{ matrix.os }}\",
\"FORTIFY_RELEASE\": \"${{ steps.fortify-release.outputs.name }}\"
}
}")
build_id=$(echo "$build_response" | jq -r '.id')
if [ "$build_id" = "null" ] || [ -z "$build_id" ]; then
echo "Error: Failed to get build ID from response:"
echo "$build_response" | jq .
exit 1
fi
build_url="https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_build/results?buildId=$build_id"
echo "Build ID: $build_id"
echo "Build URL: $build_url"
echo "Polling for completion (timeout: ${{ github.event.inputs.timeout_minutes }} minutes)..."
timeout=$((60 * ${{ github.event.inputs.timeout_minutes }}))
elapsed=0
poll_interval=30
check_interval=5
test_passed=false
while [ $elapsed -lt $timeout ]; do
check_parent_cancelled
build_info=$(curl -s \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/build/builds/$build_id?api-version=7.1-preview.7" \
-H "$AUTH_HEADER")
build_status=$(echo "$build_info" | jq -r '.status')
build_result=$(echo "$build_info" | jq -r '.result')
echo "[$elapsed s] Status: $build_status, Result: $build_result"
if [ "$build_status" = "completed" ]; then
if [ "$build_result" = "succeeded" ]; then
echo "✓ Test passed in repo '$REPO_TAG'!"
echo "::notice::test success: ${{ steps.pipeline-name.outputs.name }} - pipeline: $build_url"
test_passed=true
else
echo "✗ Test failed in repo '$REPO_TAG' with result: $build_result"
echo "::error::test failure: ${{ steps.pipeline-name.outputs.name }} - pipeline: $build_url"
# Get failed job logs
curl -s \
"https://dev.azure.com/$ADO_ORG/$ADO_PROJ/_apis/build/builds/$build_id/timeline?api-version=7.1-preview.2" \
-H "$AUTH_HEADER" | \
jq '.records[] | select(.result == "failed") | {name, type, result}' || true
test_passed=false
fi
break
fi
# Sleep in small chunks to check for cancellation more frequently
sleep_remaining=$poll_interval
while [ $sleep_remaining -gt 0 ] && [ $elapsed -lt $timeout ]; do
sleep $check_interval
elapsed=$((elapsed + check_interval))
sleep_remaining=$((sleep_remaining - check_interval))
# Check for cancellation during sleep
check_parent_cancelled
done
done
if [ $elapsed -ge $timeout ]; then
echo "✗ Test timed out after $timeout seconds in repo '$REPO_TAG'"
echo "::error::test failure: ${{ steps.pipeline-name.outputs.name }} - pipeline: $build_url (timeout)"
test_passed=false
fi
# Fail the job if test failed
if [ "$test_passed" != "true" ]; then
echo "ERROR: Test failed"
exit 1
fi