|
| 1 | +# Monthly bill reconcile — fork this repo, drop in your export, never think |
| 2 | +# about it again (the "fork-to-run" pattern: GitHub Actions is the compute, |
| 3 | +# there is no server and nothing leaves your fork). |
| 4 | +# |
| 5 | +# Setup (5 minutes, one time): |
| 6 | +# 1. Fork this repo (a PRIVATE fork keeps your export to yourself). |
| 7 | +# 2. Copy examples/reconcile.example.json to .agentmeasure/reconcile.json |
| 8 | +# and set "vendor" (agentmeasure recount --list-vendors shows the ids). |
| 9 | +# 3. Export last month's resolution counts from your vendor and save them as |
| 10 | +# .agentmeasure/recount-export.csv (the 7-column export; see RECONCILE.md). |
| 11 | +# 4. Commit both files to your fork. |
| 12 | +# 5. Run it once from the Actions tab ("Monthly bill reconcile" → Run |
| 13 | +# workflow) to confirm, then let the monthly schedule take over. |
| 14 | +# |
| 15 | +# Every run: recount your export under the vendor's own published rules, |
| 16 | +# pin the report to the run summary, upload the machine-readable result as an |
| 17 | +# artifact, and open one tracking issue per billing month when the net |
| 18 | +# variance is not zero. |
| 19 | + |
| 20 | +name: Monthly bill reconcile |
| 21 | + |
| 22 | +on: |
| 23 | + schedule: |
| 24 | + # 3rd of each month, 09:00 UTC — after vendor invoices close. |
| 25 | + - cron: "0 9 3 * *" |
| 26 | + workflow_dispatch: {} |
| 27 | + |
| 28 | +permissions: |
| 29 | + contents: read |
| 30 | + issues: write |
| 31 | + |
| 32 | +concurrency: |
| 33 | + group: reconcile-${{ github.ref }} |
| 34 | + cancel-in-progress: true |
| 35 | + |
| 36 | +jobs: |
| 37 | + reconcile: |
| 38 | + runs-on: ubuntu-latest |
| 39 | + steps: |
| 40 | + - uses: actions/checkout@v4 |
| 41 | + |
| 42 | + - uses: actions/setup-python@v5 |
| 43 | + with: |
| 44 | + python-version: "3.11" |
| 45 | + |
| 46 | + - name: Install AgentMeasure |
| 47 | + run: pip install agentmeasure |
| 48 | + |
| 49 | + - name: Read configuration |
| 50 | + id: cfg |
| 51 | + run: | |
| 52 | + if [ ! -f .agentmeasure/reconcile.json ]; then |
| 53 | + echo "::notice::No .agentmeasure/reconcile.json found. Copy examples/reconcile.example.json, set your vendor, and commit your export as .agentmeasure/recount-export.csv (see RECONCILE.md). Nothing was run." |
| 54 | + echo "configured=false" >> "$GITHUB_OUTPUT" |
| 55 | + exit 0 |
| 56 | + fi |
| 57 | + echo "configured=true" >> "$GITHUB_OUTPUT" |
| 58 | + python - << 'PY' >> "$GITHUB_OUTPUT" |
| 59 | + import json |
| 60 | + cfg = json.load(open(".agentmeasure/reconcile.json")) |
| 61 | + print("vendor=%s" % cfg.get("vendor", "")) |
| 62 | + print("export=%s" % cfg.get("export", ".agentmeasure/recount-export.csv")) |
| 63 | + PY |
| 64 | +
|
| 65 | + - name: Recount the export |
| 66 | + if: steps.cfg.outputs.configured == 'true' |
| 67 | + run: | |
| 68 | + EXPORT="${{ steps.cfg.outputs.export }}" |
| 69 | + if [ ! -f "$EXPORT" ]; then |
| 70 | + echo "::error::$EXPORT not found. Save your vendor export there first (RECONCILE.md describes the 7 columns)." |
| 71 | + exit 1 |
| 72 | + fi |
| 73 | + agentmeasure recount \ |
| 74 | + --export "$EXPORT" \ |
| 75 | + --vendor "${{ steps.cfg.outputs.vendor }}" \ |
| 76 | + --json recount-result.json | tee recount-report.txt |
| 77 | +
|
| 78 | + - name: Pin report to the run summary |
| 79 | + if: steps.cfg.outputs.configured == 'true' |
| 80 | + run: | |
| 81 | + { |
| 82 | + echo "## Monthly bill reconcile" |
| 83 | + echo |
| 84 | + cat recount-report.txt |
| 85 | + } >> "$GITHUB_STEP_SUMMARY" |
| 86 | +
|
| 87 | + - name: Upload machine-readable result |
| 88 | + if: steps.cfg.outputs.configured == 'true' |
| 89 | + uses: actions/upload-artifact@v4 |
| 90 | + with: |
| 91 | + name: reconcile-result |
| 92 | + path: | |
| 93 | + recount-report.txt |
| 94 | + recount-result.json |
| 95 | + if-no-files-found: error |
| 96 | + |
| 97 | + - name: Open a tracking issue when the variance is not zero |
| 98 | + if: steps.cfg.outputs.configured == 'true' |
| 99 | + env: |
| 100 | + GH_TOKEN: ${{ github.token }} |
| 101 | + run: | |
| 102 | + python - << 'PY' > /tmp/verdict.txt |
| 103 | + import json |
| 104 | + r = json.load(open("recount-result.json")) |
| 105 | + if "variance" in r: |
| 106 | + print("variance=%.2f" % r["variance"]) |
| 107 | + else: |
| 108 | + print("variance=none") |
| 109 | + PY |
| 110 | + VERDICT="$(cat /tmp/verdict.txt)" |
| 111 | + case "$VERDICT" in |
| 112 | + variance=0.00) |
| 113 | + echo "Net variance is zero — nothing to track." |
| 114 | + exit 0 |
| 115 | + ;; |
| 116 | + variance=none) |
| 117 | + echo "Vendor price not published — no dollar variance computed; see the report for counts." |
| 118 | + exit 0 |
| 119 | + ;; |
| 120 | + esac |
| 121 | + TITLE="Bill reconcile: net variance $VERDICT (via ${{ steps.cfg.outputs.vendor }} rules)" |
| 122 | + BODY_FILE=/tmp/issue-body.md |
| 123 | + { |
| 124 | + echo "Monthly recount found a non-zero net variance." |
| 125 | + echo |
| 126 | + echo '```' |
| 127 | + cat recount-report.txt |
| 128 | + echo '```' |
| 129 | + echo |
| 130 | + echo "_Automated by the Monthly bill reconcile workflow. The dollar variance is arithmetic under the vendor's own published rules; it is evidence for a conversation with the vendor, not a guaranteed refund. See RECONCILE.md._" |
| 131 | + } > "$BODY_FILE" |
| 132 | + gh issue list --state open --search "Bill reconcile: in:title" --json number,title \ |
| 133 | + --jq '.[] | select(.title | startswith("Bill reconcile:")) | .number' \ |
| 134 | + > /tmp/open-issue.txt || true |
| 135 | + EXISTING="$(head -n1 /tmp/open-issue.txt || true)" |
| 136 | + if [ -n "$EXISTING" ]; then |
| 137 | + gh issue comment "$EXISTING" --body-file "$BODY_FILE" |
| 138 | + else |
| 139 | + gh issue create --title "$TITLE" --body-file "$BODY_FILE" |
| 140 | + fi |
0 commit comments