From d2d5dd6648f0a4d61b6f31cf486605519e2ac066 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Thu, 26 Mar 2026 16:58:26 +0100 Subject: [PATCH 1/6] Add reusable check_dependencies workflow for consumer repos Expose as workflow_call only; includes check-auth then per-handle silverfin check-dependencies with PR comments. Callers pass triggers (pull_request labeled, workflow_dispatch) and secrets: inherit. Made-with: Cursor --- .github/workflows/check_dependencies.yml | 221 +++++++++++++++++++++++ 1 file changed, 221 insertions(+) create mode 100644 .github/workflows/check_dependencies.yml diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml new file mode 100644 index 0000000..5ec86d5 --- /dev/null +++ b/.github/workflows/check_dependencies.yml @@ -0,0 +1,221 @@ +# 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. +# Runs check_auth first to refresh tokens (same pattern as run_tests). +name: Check dependencies +run-name: Check dependencies for changed reconciliation templates +on: + workflow_call: + +jobs: + check-auth: + # Refresh Silverfin API tokens (same as run_tests) + if: github.event.label.name == 'code-review' || github.event_name == 'workflow_dispatch' + uses: ./.github/workflows/check_auth.yml + secrets: inherit + + 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' + needs: [check-auth] + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + env: + SF_API_CLIENT_ID: "${{ secrets.SF_API_CLIENT_ID }}" + SF_API_SECRET: "${{ secrets.SF_API_SECRET }}" + steps: + # Resolve PR base SHA (required for checkout; undefined for workflow_dispatch) + - name: Get PR details + id: pr-details + uses: actions/github-script@v7 + with: + script: | + const prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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); + + # Check out base branch (used for config.json / handle resolution; file list from API) + - name: Checkout base branch + uses: actions/checkout@v4 + with: + ref: ${{ steps.pr-details.outputs.base_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 prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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 + run: | + changed_files="${{ steps.pr-files.outputs.paths }}" + 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 prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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 + + - name: Load Silverfin config + if: steps.handles.outputs.handles_json != '[]' + run: | + mkdir -p $HOME/.silverfin/ + echo '${{ secrets.CONFIG_JSON }}' > $HOME/.silverfin/config.json + + # 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 + for handle in $(echo "$HANDLES_JSON" | jq -r ".[]"); 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 + 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 prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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 From 28d11647876c191e9636dc8eaa32995e686e3221 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Thu, 26 Mar 2026 17:07:30 +0100 Subject: [PATCH 2/6] Drop check_auth from check_dependencies to avoid secret-update emails check_auth refreshes CONFIG_JSON via gh secret set, which emails admins. silverfin check-dependencies only reads local Liquid Test YAML and does not need API config or tokens. Made-with: Cursor --- .github/workflows/check_dependencies.yml | 21 ++++----------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 5ec86d5..cfb1efe 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -1,30 +1,23 @@ # 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. -# Runs check_auth first to refresh tokens (same pattern as run_tests). +# +# 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: jobs: - check-auth: - # Refresh Silverfin API tokens (same as run_tests) - if: github.event.label.name == 'code-review' || github.event_name == 'workflow_dispatch' - uses: ./.github/workflows/check_auth.yml - secrets: inherit - 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' - needs: [check-auth] runs-on: ubuntu-latest permissions: contents: read pull-requests: write - env: - SF_API_CLIENT_ID: "${{ secrets.SF_API_CLIENT_ID }}" - SF_API_SECRET: "${{ secrets.SF_API_SECRET }}" steps: # Resolve PR base SHA (required for checkout; undefined for workflow_dispatch) - name: Get PR details @@ -131,12 +124,6 @@ jobs: npm install https://github.com/silverfin/silverfin-cli.git node ./node_modules/silverfin-cli/bin/cli.js -V - - name: Load Silverfin config - if: steps.handles.outputs.handles_json != '[]' - run: | - mkdir -p $HOME/.silverfin/ - echo '${{ secrets.CONFIG_JSON }}' > $HOME/.silverfin/config.json - # Run check-dependencies for each handle and collect results - name: Run check-dependencies per handle id: run-check From 553f7fbe3f4ab7509951a3029c9e3ea07318281d Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Thu, 26 Mar 2026 17:10:58 +0100 Subject: [PATCH 3/6] Define workflow_call input pull_request_number and resolve PR in scripts Optional string input documents the caller contract; scripts use payload.pull_request, payload.inputs, then WORKFLOW_CALL_PR_NUMBER from inputs. Made-with: Cursor --- .github/workflows/check_dependencies.yml | 31 +++++++++++++++++++++--- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index cfb1efe..7e085cb 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -9,6 +9,11 @@ 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: @@ -18,6 +23,8 @@ jobs: permissions: contents: read pull-requests: write + env: + WORKFLOW_CALL_PR_NUMBER: ${{ inputs.pull_request_number }} steps: # Resolve PR base SHA (required for checkout; undefined for workflow_dispatch) - name: Get PR details @@ -25,7 +32,11 @@ jobs: uses: actions/github-script@v7 with: script: | - const prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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, @@ -45,7 +56,11 @@ jobs: uses: actions/github-script@v7 with: script: | - const prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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, @@ -101,7 +116,11 @@ jobs: marker ].join("\n"); const { owner, repo } = context.repo; - const prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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 }); @@ -178,7 +197,11 @@ jobs: marker ].join("\n"); - const prNumber = context.payload.pull_request?.number || context.payload.inputs?.pull_request_number; + 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; From 1242db0890b1652a58f95cfc00f5ff0dd94d6c07 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Thu, 26 Mar 2026 17:14:48 +0100 Subject: [PATCH 4/6] Pass PR file list via CHANGED_FILES env in handle-resolution step Avoids embedding paths in the shell script body (metacharacter safety). Made-with: Cursor --- .github/workflows/check_dependencies.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index 7e085cb..b5c3009 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -72,8 +72,10 @@ jobs: # 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="${{ steps.pr-files.outputs.paths }}" + 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)) From b96d36ee1c12430cd7c70caa959b2cf6ff6c4a5b Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Thu, 26 Mar 2026 17:21:14 +0100 Subject: [PATCH 5/6] Checkout PR head SHA for check-dependencies workspace Resolve handles and config.json from the PR branch, not the merge base. Keep base_sha output for potential future use. Made-with: Cursor --- .github/workflows/check_dependencies.yml | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index b5c3009..a79ee96 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -26,7 +26,7 @@ jobs: env: WORKFLOW_CALL_PR_NUMBER: ${{ inputs.pull_request_number }} steps: - # Resolve PR base SHA (required for checkout; undefined for workflow_dispatch) + # 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 @@ -43,12 +43,13 @@ jobs: pull_number: prNumber }); core.setOutput("base_sha", pr.base.sha); + core.setOutput("head_sha", pr.head.sha); - # Check out base branch (used for config.json / handle resolution; file list from API) - - name: Checkout base branch + # 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.base_sha }} + 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 From 08f922b0614c72a88cbd723785ad7f8c8da52d63 Mon Sep 17 00:00:00 2001 From: BenjaminLangenakenSF Date: Thu, 26 Mar 2026 17:25:59 +0100 Subject: [PATCH 6/6] Iterate handles with read loop to avoid word-splitting on jq output Made-with: Cursor --- .github/workflows/check_dependencies.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/check_dependencies.yml b/.github/workflows/check_dependencies.yml index a79ee96..ef1c532 100644 --- a/.github/workflows/check_dependencies.yml +++ b/.github/workflows/check_dependencies.yml @@ -155,7 +155,7 @@ jobs: run: | job_failed=0 : > check_results.txt - for handle in $(echo "$HANDLES_JSON" | jq -r ".[]"); do + 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 @@ -175,7 +175,7 @@ jobs: echo "**Status: OK**" >> check_results.txt fi echo "" >> check_results.txt - done + done < <(jq -r '.[]' <<< "$HANDLES_JSON") echo "results<> $GITHUB_OUTPUT cat check_results.txt >> $GITHUB_OUTPUT echo "CHECK_EOF" >> $GITHUB_OUTPUT