Fix Veeam x-api-version header sent as [object Object] #407
Workflow file for this run
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: Validate & Deploy Plugins | |
| on: | |
| pull_request: | |
| jobs: | |
| validate-and-deploy: | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| steps: | |
| - uses: actions/checkout@v6 | |
| with: | |
| fetch-depth: 0 | |
| - name: Detect modified plugins | |
| id: detect | |
| run: | | |
| echo "Scanning for modified plugins against origin/main..." | |
| changed_files=$(git diff --name-only origin/main... -- ./plugins) | |
| plugin_paths=$(echo "$changed_files" | grep -oP 'plugins/[^/]+/v[^/]+' | sort -u) | |
| if [ -z "$plugin_paths" ]; then | |
| echo "No plugins were modified in this PR." | |
| echo "plugins_modified=false" >> $GITHUB_OUTPUT | |
| echo "existing_modified=false" >> $GITHUB_OUTPUT | |
| exit 0 | |
| fi | |
| echo "Found modified plugin(s):" | |
| echo "$plugin_paths" | |
| # A PR that deletes a plugin version folder still lists it as touched, but it | |
| # cannot be validated or deployed - the CLI reports "Path not found". Split the | |
| # paths that still exist so only those reach the version, validate and deploy | |
| # steps; the full list stays for the one-plugin-per-PR check, which must still | |
| # count a deleted plugin as touched. | |
| existing_paths=$(while IFS= read -r p; do if [ -d "$p" ]; then printf '%s\n' "$p"; fi; done <<< "$plugin_paths") | |
| removed_paths=$(while IFS= read -r p; do if [ ! -d "$p" ]; then printf '%s\n' "$p"; fi; done <<< "$plugin_paths") | |
| if [ -n "$removed_paths" ]; then | |
| echo "Removed in this PR (skipped for validate and deploy):" | |
| echo "$removed_paths" | |
| fi | |
| echo "plugins_modified=true" >> $GITHUB_OUTPUT | |
| echo "plugin_paths<<EOF" >> $GITHUB_OUTPUT | |
| echo "$plugin_paths" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| if [ -n "$existing_paths" ]; then | |
| echo "existing_modified=true" >> $GITHUB_OUTPUT | |
| echo "existing_paths<<EOF" >> $GITHUB_OUTPUT | |
| echo "$existing_paths" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| else | |
| echo "existing_modified=false" >> $GITHUB_OUTPUT | |
| fi | |
| # Plugin paths come from the contributor's branch, so they are untrusted input. | |
| # They are passed through env rather than interpolated into the script | |
| - name: Check PR scope & version bumps | |
| id: checks | |
| if: steps.detect.outputs.plugins_modified == 'true' | |
| env: | |
| PLUGIN_PATHS: ${{ steps.detect.outputs.plugin_paths }} | |
| EXISTING_PATHS: ${{ steps.detect.outputs.existing_paths }} | |
| run: | | |
| checks_failed=false | |
| # One plugin per PR. Several version folders of the same plugin are fine, | |
| # so that deprecating in v1 alongside adding v2 stays a single PR. | |
| plugin_names=$(printf '%s\n' "$PLUGIN_PATHS" | cut -d/ -f2 | sort -u) | |
| plugin_count=$(echo "$plugin_names" | wc -l | tr -d ' ') | |
| if [ "$plugin_count" -gt 1 ]; then | |
| echo "[FAIL] This PR modifies ${plugin_count} plugins:" | |
| echo "$plugin_names" | sed 's/^/ - /' | |
| echo "Raise one pull request per plugin - see REVIEW.md." | |
| checks_failed=true | |
| else | |
| echo "[PASS] Single plugin modified: ${plugin_names}" | |
| fi | |
| echo "" | |
| # Every change to an existing plugin must increase metadata.json version. | |
| while IFS= read -r plugin_path; do | |
| if [ -z "$plugin_path" ]; then | |
| continue | |
| fi | |
| if [ ! -f "${plugin_path}/metadata.json" ]; then | |
| echo "[FAIL] ${plugin_path} has no metadata.json" | |
| checks_failed=true | |
| continue | |
| fi | |
| new_version=$(jq -r '.version' "${plugin_path}/metadata.json") | |
| # The major version must match the version folder: v2/ holds 2.x.y | |
| folder_major=$(basename "${plugin_path}") | |
| folder_major=${folder_major#v} | |
| if [ "${new_version%%.*}" != "$folder_major" ]; then | |
| echo "[FAIL] ${plugin_path} is version ${new_version} - a plugin in v${folder_major}/ must be ${folder_major}.x.y" | |
| checks_failed=true | |
| fi | |
| if ! old_metadata=$(git show "origin/main:${plugin_path}/metadata.json" 2>/dev/null); then | |
| echo "[SKIP] ${plugin_path} is new - no previous version to compare" | |
| continue | |
| fi | |
| old_version=$(echo "$old_metadata" | jq -r '.version') | |
| if [ "$new_version" = "$old_version" ]; then | |
| echo "[FAIL] ${plugin_path} version is unchanged (${old_version}) - bump it in metadata.json" | |
| checks_failed=true | |
| elif [ "$(printf '%s\n%s\n' "$old_version" "$new_version" | sort -V | head -1)" != "$old_version" ]; then | |
| echo "[FAIL] ${plugin_path} version decreased (${old_version} -> ${new_version}) - it can never decrease" | |
| checks_failed=true | |
| else | |
| echo "[PASS] ${plugin_path} version bumped ${old_version} -> ${new_version}" | |
| fi | |
| done <<< "$EXISTING_PATHS" | |
| if [ "$checks_failed" = "true" ]; then | |
| echo "" | |
| echo "One or more repository checks failed." | |
| exit 1 | |
| fi | |
| - name: Install & Configure SquaredUp CLI | |
| if: steps.detect.outputs.existing_modified == 'true' | |
| env: | |
| SQUAREDUP_API_KEY: ${{ secrets.SQUAREDUP_API_KEY }} | |
| run: | | |
| echo "Installing SquaredUp CLI..." | |
| npm install -g @squaredup/cli | |
| echo "Configuring SquaredUp CLI with API key..." | |
| squaredup login --apiKey "$SQUAREDUP_API_KEY" | |
| - name: Validate modified plugins | |
| id: validate | |
| if: steps.detect.outputs.existing_modified == 'true' | |
| env: | |
| EXISTING_PATHS: ${{ steps.detect.outputs.existing_paths }} | |
| run: | | |
| validation_failed=false | |
| while IFS= read -r plugin_path; do | |
| echo "Validating ${plugin_path}..." | |
| result=$(squaredup validate "${plugin_path}" --json) || true | |
| echo "$result" | jq '.' | |
| valid=$(echo "$result" | jq -r '.valid') | |
| echo "$result" | jq -c --arg path "$plugin_path" '. + {plugin_path: $path}' >> /tmp/validation_results.ndjson | |
| if [ "$valid" = "true" ]; then | |
| plugin_name=$(echo "$result" | jq -r '.pluginName') | |
| echo "[PASS] ${plugin_name} passed validation" | |
| echo "$result" | jq '.summary' | |
| else | |
| echo "[FAIL] Validation failed for ${plugin_path}" | |
| echo "$result" | jq -r '.errors[] | " - [\(.file)] \(.message) (path: \(.path | join(".")))"' | |
| validation_failed=true | |
| fi | |
| echo "" | |
| done <<< "$EXISTING_PATHS" | |
| if [ "$validation_failed" = "true" ]; then | |
| echo "One or more plugins failed validation." | |
| exit 1 | |
| fi | |
| - name: Deploy modified plugins | |
| id: deploy | |
| if: steps.detect.outputs.existing_modified == 'true' | |
| env: | |
| EXISTING_PATHS: ${{ steps.detect.outputs.existing_paths }} | |
| run: | | |
| while IFS= read -r plugin_path; do | |
| echo "Deploying ${plugin_path}..." | |
| squaredup deploy "${plugin_path}" --suffix "${{ github.event.pull_request.number }}" --force | |
| echo "Deployed ${plugin_path} successfully." | |
| echo "" | |
| done <<< "$EXISTING_PATHS" | |
| - name: Summary | |
| if: always() | |
| env: | |
| PLUGIN_PATHS: ${{ steps.detect.outputs.plugin_paths }} | |
| run: | | |
| OUT=/tmp/summary.md | |
| echo "## 🧩 Plugin PR Summary" >> $OUT | |
| echo "" >> $OUT | |
| if [ "${{ steps.detect.outputs.plugins_modified }}" != "true" ]; then | |
| echo "ℹ️ No plugins were modified in this PR." >> $OUT | |
| cat $OUT >> $GITHUB_STEP_SUMMARY | |
| exit 0 | |
| fi | |
| echo "### 📦 Modified Plugins" >> $OUT | |
| while IFS= read -r plugin_path; do | |
| echo "- \`${plugin_path}\`" >> $OUT | |
| done <<< "$PLUGIN_PATHS" | |
| echo "" >> $OUT | |
| echo "### 📋 Results" >> $OUT | |
| echo "| Step | Status |" >> $OUT | |
| echo "|------|--------|" >> $OUT | |
| # A failed earlier step skips the later ones, so report skipped as skipped | |
| # rather than folding it into a failure. | |
| status_of() { | |
| case "$1" in | |
| success) echo "✅ Passed" ;; | |
| skipped|"") echo "⏭️ Skipped" ;; | |
| *) echo "❌ Failed" ;; | |
| esac | |
| } | |
| deploy_conclusion="${{ steps.deploy.conclusion }}" | |
| [ "$deploy_conclusion" = "success" ] && d_status="🚀 Deployed" || d_status="$(status_of "$deploy_conclusion")" | |
| echo "| Scope & version | $(status_of "${{ steps.checks.conclusion }}") |" >> $OUT | |
| echo "| Validation | $(status_of "${{ steps.validate.conclusion }}") |" >> $OUT | |
| echo "| Deployment | ${d_status} |" >> $OUT | |
| echo "" >> $OUT | |
| if [ -f /tmp/validation_results.ndjson ]; then | |
| echo "### 🔍 Validation Details" >> $OUT | |
| echo "" >> $OUT | |
| while IFS= read -r line; do | |
| plugin_path=$(echo "$line" | jq -r '.plugin_path') | |
| valid=$(echo "$line" | jq -r '.valid') | |
| plugin_name=$(echo "$line" | jq -r '.pluginName // .plugin_path') | |
| [ "$valid" = "true" ] && icon="✅" || icon="❌" | |
| echo "<details>" >> $OUT | |
| echo "<summary>${icon} <code>${plugin_name}</code></summary>" >> $OUT | |
| echo "" >> $OUT | |
| echo '```json' >> $OUT | |
| echo "$line" | jq 'del(.plugin_path)' >> $OUT | |
| echo '```' >> $OUT | |
| echo "</details>" >> $OUT | |
| echo "" >> $OUT | |
| done < /tmp/validation_results.ndjson | |
| fi | |
| cat $OUT >> $GITHUB_STEP_SUMMARY | |
| - name: Post PR comment | |
| if: always() | |
| uses: actions/github-script@v7 | |
| with: | |
| script: | | |
| const fs = require('fs'); | |
| const marker = '<!-- plugin-pr-summary -->'; | |
| const summary = fs.existsSync('/tmp/summary.md') | |
| ? fs.readFileSync('/tmp/summary.md', 'utf8') | |
| : '_No summary available._'; | |
| const body = `${marker}\n${summary}`; | |
| const comments = await github.paginate(github.rest.issues.listComments, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| }); | |
| const existing = comments.find(c => c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| }); | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); |