Run Integration Tests #112
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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: | | |
| always() && | |
| 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_status=$(gh run view ${{ github.run_id }} --json status --jq '.status') | |
| if [ "$run_status" = "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 | |
| # Trap termination signals for graceful cancellation | |
| trap 'echo "✗ Received termination signal, exiting..."; exit 143' TERM INT | |
| # Store parent workflow token for cancellation checks | |
| PARENT_GH_TOKEN="${{ github.token }}" | |
| # 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 "====================================" | |
| # 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 }} | |
| 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 if this workflow run has been cancelled | |
| parent_run_status=$(GH_TOKEN="$PARENT_GH_TOKEN" gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json status --jq '.status' 2>/dev/null || echo "unknown") | |
| echo "[DEBUG] Parent workflow status check: $parent_run_status" | |
| if [ "$parent_run_status" = "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 | |
| 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 | |
| parent_run_status=$(GH_TOKEN="$PARENT_GH_TOKEN" gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json status --jq '.status' 2>/dev/null || echo "unknown") | |
| echo "[DEBUG] Parent workflow status check (during sleep): $parent_run_status" | |
| if [ "$parent_run_status" = "cancelled" ]; then | |
| echo "✗ Parent workflow run has been cancelled during sleep, stopping test in repo '$REPO_TAG'" | |
| echo "::warning::test cancelled: ${{ steps.pipeline-name.outputs.name }} - pipeline: $run_url" | |
| exit 1 | |
| fi | |
| 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 | |
| # Trap termination signals for graceful cancellation | |
| trap 'echo "✗ Received termination signal, exiting..."; exit 143' TERM INT | |
| # 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 if this workflow run has been cancelled | |
| parent_run_status=$(gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json status --jq '.status' 2>/dev/null || echo "unknown") | |
| echo "[DEBUG] Parent workflow status check: $parent_run_status" | |
| if [ "$parent_run_status" = "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 | |
| 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 | |
| parent_run_status=$(gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json status --jq '.status' 2>/dev/null || echo "unknown") | |
| echo "[DEBUG] Parent workflow status check (during sleep): $parent_run_status" | |
| if [ "$parent_run_status" = "cancelled" ]; then | |
| echo "✗ Parent workflow run has been cancelled during sleep, stopping test in repo '$REPO_TAG'" | |
| echo "::warning::test cancelled: ${{ steps.pipeline-name.outputs.name }} - pipeline: $pipeline_url" | |
| exit 1 | |
| fi | |
| 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 | |
| # Trap termination signals for graceful cancellation | |
| trap 'echo "✗ Received termination signal, exiting..."; exit 143' TERM INT | |
| # 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 if this workflow run has been cancelled | |
| parent_run_status=$(gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json status --jq '.status' 2>/dev/null || echo "unknown") | |
| echo "[DEBUG] Parent workflow status check: $parent_run_status" | |
| if [ "$parent_run_status" = "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 | |
| 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 | |
| parent_run_status=$(gh run view ${{ github.run_id }} --repo ${{ github.repository }} --json status --jq '.status' 2>/dev/null || echo "unknown") | |
| echo "[DEBUG] Parent workflow status check (during sleep): $parent_run_status" | |
| if [ "$parent_run_status" = "cancelled" ]; then | |
| echo "✗ Parent workflow run has been cancelled during sleep, stopping test in repo '$REPO_TAG'" | |
| echo "::warning::test cancelled: ${{ steps.pipeline-name.outputs.name }} - pipeline: $build_url" | |
| exit 1 | |
| fi | |
| 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 |