diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml new file mode 100644 index 0000000..ef1c532 --- /dev/null +++ b/.github/workflows/check_dependencies.yml @@ -0,0 +1,234 @@ +# Runs `silverfin check-dependencies -h ` for each reconciliation template +# changed in the PR. Triggered only when the `code-review` label is added. +# Reusable workflow: call from a consumer repo with pull_request (labeled) and/or workflow_dispatch. +# +# Does not run check_auth: that job updates CONFIG_JSON via gh secret set, which notifies +# repo/org admins by email. The CLI check-dependencies command only scans local Liquid Test +# YAML files and does not need API credentials. +name: Check dependencies +run-name: Check dependencies for changed reconciliation templates +on: + workflow_call: + inputs: + pull_request_number: + description: "Optional PR number when the caller passes it explicitly (e.g. workflow_dispatch input)." + required: false + type: string + +jobs: + check-dependencies: + # Run when the added label is "code-review" (pull_request) or when triggered manually (workflow_dispatch) + if: github.event.label.name == 'code-review' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + WORKFLOW_CALL_PR_NUMBER: ${{ inputs.pull_request_number }} + steps: + # Resolve PR base and head SHAs (head used for checkout so scans match the PR branch) + - name: Get PR details + id: pr-details + uses: actions/github-script@v7 + with: + script: | + const fromCall = process.env.WORKFLOW_CALL_PR_NUMBER; + const prNumber = + context.payload.pull_request?.number ?? + context.payload.inputs?.pull_request_number ?? + (fromCall && fromCall !== "" ? parseInt(fromCall, 10) : undefined); + const { data: pr } = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + core.setOutput("base_sha", pr.base.sha); + core.setOutput("head_sha", pr.head.sha); + + # Check out PR HEAD (config.json / template tree for check-dependencies must match the PR) + - name: Checkout PR head + uses: actions/checkout@v4 + with: + ref: ${{ steps.pr-details.outputs.head_sha }} + + # Get list of files changed in the PR via API (no PR checkout) + - name: Get PR changed files + id: pr-files + uses: actions/github-script@v7 + with: + script: | + const fromCall = process.env.WORKFLOW_CALL_PR_NUMBER; + const prNumber = + context.payload.pull_request?.number ?? + context.payload.inputs?.pull_request_number ?? + (fromCall && fromCall !== "" ? parseInt(fromCall, 10) : undefined); + const files = await github.paginate(github.rest.pulls.listFiles, { + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber + }); + const paths = files.map(f => f.filename).join("\n"); + core.setOutput("paths", paths); + + # Derive reconciliation handles from changed paths (same pattern as run_tests "Filter templates changed") + - name: Get reconciliation handles to check + id: handles + env: + CHANGED_FILES: ${{ steps.pr-files.outputs.paths }} + run: | + changed_files="$CHANGED_FILES" + pattern="reconciliation_texts/([^/]+)/" + if [ -n "$changed_files" ]; then + filtered_names=($(printf "%s\n" "$changed_files" | grep -oE "$pattern" | sed "s|reconciliation_texts/||;s|/||" | sort -u)) + else + filtered_names=() + fi + # Resolve handle from config.json if present (explicit mapping), else use directory name + handles=() + for dir in "${filtered_names[@]}"; do + config_path="reconciliation_texts/${dir}/config.json" + if [ -f "$config_path" ]; then + h=$(jq -r ".handle // .name // empty" "$config_path" 2>/dev/null || true) + [ -z "$h" ] && h="$dir" + else + h="$dir" + fi + handles+=("$h") + done + # Dedupe and output + if [ ${#handles[@]} -eq 0 ]; then + echo "handles_json=[]" >> $GITHUB_OUTPUT + echo "No reconciliation templates changed." + else + echo "handles_json=$(printf "%s\n" "${handles[@]}" | sort -u | jq -R -s -c "split(\"\n\") | map(select(length > 0))")" >> $GITHUB_OUTPUT + echo "Handles to check:" + printf "%s\n" "${handles[@]}" | sort -u + fi + + - name: Post comment when no reconciliation templates changed + if: steps.handles.outputs.handles_json == '[]' + uses: actions/github-script@v7 + with: + script: | + const marker = ""; + const body = [ + "## Silverfin check-dependencies", + "", + "No reconciliation templates were changed in this PR. Nothing to run.", + "", + marker + ].join("\n"); + const { owner, repo } = context.repo; + const fromCall = process.env.WORKFLOW_CALL_PR_NUMBER; + const prNumber = + context.payload.pull_request?.number ?? + context.payload.inputs?.pull_request_number ?? + (fromCall && fromCall !== "" ? parseInt(fromCall, 10) : undefined); + const { data: comments } = await github.rest.issues.listComments({ + owner, repo, issue_number: prNumber + }); + const existing = comments.find(c => + c.user.type === "Bot" && c.body && c.body.includes(marker) + ); + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: existing.id, body + }); + } else { + await github.rest.issues.createComment({ + owner, repo, issue_number: prNumber, body + }); + } + + - name: Setup Node and Silverfin CLI + if: steps.handles.outputs.handles_json != '[]' + run: | + npm install https://github.com/silverfin/silverfin-cli.git + node ./node_modules/silverfin-cli/bin/cli.js -V + + # Run check-dependencies for each handle and collect results + - name: Run check-dependencies per handle + id: run-check + if: steps.handles.outputs.handles_json != '[]' + env: + HANDLES_JSON: ${{ steps.handles.outputs.handles_json }} + run: | + job_failed=0 + : > check_results.txt + while IFS= read -r handle; do + echo "## Handle: \`${handle}\`" >> check_results.txt + echo "" >> check_results.txt + echo "Command: \`silverfin check-dependencies -h ${handle}\`" >> check_results.txt + echo "" >> check_results.txt + set +e + output=$(node ./node_modules/silverfin-cli/bin/cli.js check-dependencies -h "$handle" 2>&1) + exit_code=$? + set -e + echo '```' >> check_results.txt + echo "$output" >> check_results.txt + echo '```' >> check_results.txt + echo "" >> check_results.txt + if [[ $exit_code -ne 0 ]]; then + echo "**Status: Failed (exit code ${exit_code})**" >> check_results.txt + job_failed=1 + else + echo "**Status: OK**" >> check_results.txt + fi + echo "" >> check_results.txt + done < <(jq -r '.[]' <<< "$HANDLES_JSON") + echo "results<> $GITHUB_OUTPUT + cat check_results.txt >> $GITHUB_OUTPUT + echo "CHECK_EOF" >> $GITHUB_OUTPUT + echo "job_failed=$job_failed" >> $GITHUB_OUTPUT + + - name: Post PR comment with results + if: steps.handles.outputs.handles_json != '[]' && always() && steps.run-check.outcome != 'skipped' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const marker = ""; + const resultsContent = fs.existsSync('check_results.txt') + ? fs.readFileSync('check_results.txt', 'utf8') + : ''; + const body = [ + "## Silverfin check-dependencies", + "", + "Ran for reconciliation templates changed in this PR (triggered by `code-review` label).", + "", + resultsContent, + marker + ].join("\n"); + + const fromCall = process.env.WORKFLOW_CALL_PR_NUMBER; + const prNumber = + context.payload.pull_request?.number ?? + context.payload.inputs?.pull_request_number ?? + (fromCall && fromCall !== "" ? parseInt(fromCall, 10) : undefined); + const owner = context.repo.owner; + const repo = context.repo.repo; + + const { data: comments } = await github.rest.issues.listComments({ + owner, repo, issue_number: prNumber + }); + const existing = comments.find(c => + c.user.type === "Bot" && c.body && c.body.includes(marker) + ); + + if (existing) { + await github.rest.issues.updateComment({ + owner, repo, comment_id: existing.id, body + }); + console.log("Updated existing check-dependencies comment"); + } else { + await github.rest.issues.createComment({ + owner, repo, issue_number: prNumber, body + }); + console.log("Created new check-dependencies comment"); + } + + - name: Fail job if any check-dependencies failed + if: steps.handles.outputs.handles_json != '[]' && steps.run-check.outputs.job_failed == '1' + run: | + echo "One or more silverfin check-dependencies runs failed. See PR comment for details." + exit 1