dbt-costgate 0.7.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. dbt_costgate-0.7.1/.editorconfig +18 -0
  2. dbt_costgate-0.7.1/.github/ISSUE_TEMPLATE/bug_report.yml +39 -0
  3. dbt_costgate-0.7.1/.github/ISSUE_TEMPLATE/config.yml +8 -0
  4. dbt_costgate-0.7.1/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
  5. dbt_costgate-0.7.1/.github/PULL_REQUEST_TEMPLATE.md +29 -0
  6. dbt_costgate-0.7.1/.github/dependabot.yml +19 -0
  7. dbt_costgate-0.7.1/.github/selftest/dbt-costgate +23 -0
  8. dbt_costgate-0.7.1/.github/selftest/gh +40 -0
  9. dbt_costgate-0.7.1/.github/workflows/action-selftest.yml +124 -0
  10. dbt_costgate-0.7.1/.github/workflows/ci.yml +49 -0
  11. dbt_costgate-0.7.1/.github/workflows/ple.yml +70 -0
  12. dbt_costgate-0.7.1/.github/workflows/release.yml +108 -0
  13. dbt_costgate-0.7.1/.gitignore +31 -0
  14. dbt_costgate-0.7.1/CHANGELOG.md +181 -0
  15. dbt_costgate-0.7.1/CODEOWNERS +12 -0
  16. dbt_costgate-0.7.1/CODE_OF_CONDUCT.md +132 -0
  17. dbt_costgate-0.7.1/CONTRIBUTING.md +96 -0
  18. dbt_costgate-0.7.1/LICENSE +201 -0
  19. dbt_costgate-0.7.1/NOTICE +25 -0
  20. dbt_costgate-0.7.1/PKG-INFO +218 -0
  21. dbt_costgate-0.7.1/README.md +191 -0
  22. dbt_costgate-0.7.1/SECURITY.md +47 -0
  23. dbt_costgate-0.7.1/action.yml +205 -0
  24. dbt_costgate-0.7.1/docs/architecture.md +103 -0
  25. dbt_costgate-0.7.1/docs/assets/flow.svg +60 -0
  26. dbt_costgate-0.7.1/docs/assets/logo.svg +16 -0
  27. dbt_costgate-0.7.1/docs/assets/pr-comment-mock.svg +70 -0
  28. dbt_costgate-0.7.1/docs/usage.md +387 -0
  29. dbt_costgate-0.7.1/pyproject.toml +54 -0
  30. dbt_costgate-0.7.1/scripts/sticky_comment.sh +55 -0
  31. dbt_costgate-0.7.1/src/dbt_costgate/__init__.py +4 -0
  32. dbt_costgate-0.7.1/src/dbt_costgate/against.py +150 -0
  33. dbt_costgate-0.7.1/src/dbt_costgate/artifacts.py +279 -0
  34. dbt_costgate-0.7.1/src/dbt_costgate/bigquery.py +110 -0
  35. dbt_costgate-0.7.1/src/dbt_costgate/cli.py +394 -0
  36. dbt_costgate-0.7.1/src/dbt_costgate/config.py +291 -0
  37. dbt_costgate-0.7.1/src/dbt_costgate/data/__init__.py +3 -0
  38. dbt_costgate-0.7.1/src/dbt_costgate/data/pricing.json +12 -0
  39. dbt_costgate-0.7.1/src/dbt_costgate/estimate.py +185 -0
  40. dbt_costgate-0.7.1/src/dbt_costgate/gitdiff.py +75 -0
  41. dbt_costgate-0.7.1/src/dbt_costgate/models.py +180 -0
  42. dbt_costgate-0.7.1/src/dbt_costgate/policy.py +63 -0
  43. dbt_costgate-0.7.1/src/dbt_costgate/pricing.py +94 -0
  44. dbt_costgate-0.7.1/src/dbt_costgate/report.py +252 -0
  45. dbt_costgate-0.7.1/tests/conftest.py +128 -0
  46. dbt_costgate-0.7.1/tests/test_against.py +248 -0
  47. dbt_costgate-0.7.1/tests/test_artifacts.py +243 -0
  48. dbt_costgate-0.7.1/tests/test_bigquery.py +50 -0
  49. dbt_costgate-0.7.1/tests/test_cli.py +400 -0
  50. dbt_costgate-0.7.1/tests/test_config.py +167 -0
  51. dbt_costgate-0.7.1/tests/test_estimate.py +111 -0
  52. dbt_costgate-0.7.1/tests/test_gitdiff.py +82 -0
  53. dbt_costgate-0.7.1/tests/test_policy.py +126 -0
  54. dbt_costgate-0.7.1/tests/test_pricing.py +82 -0
  55. dbt_costgate-0.7.1/tests/test_report.py +153 -0
  56. dbt_costgate-0.7.1/tests/test_scaffold.py +21 -0
  57. dbt_costgate-0.7.1/tests/test_sticky_comment.py +122 -0
@@ -0,0 +1,18 @@
1
+ root = true
2
+
3
+ [*]
4
+ charset = utf-8
5
+ end_of_line = lf
6
+ insert_final_newline = true
7
+ trim_trailing_whitespace = true
8
+ indent_style = space
9
+ indent_size = 2
10
+
11
+ [*.py]
12
+ indent_size = 4
13
+
14
+ [*.md]
15
+ trim_trailing_whitespace = false
16
+
17
+ [Makefile]
18
+ indent_style = tab
@@ -0,0 +1,39 @@
1
+ name: Bug report
2
+ description: A cost estimate is wrong, the gate misbehaves, or something does not work as documented.
3
+ labels: ["bug", "triage"]
4
+ body:
5
+ - type: markdown
6
+ attributes:
7
+ value: |
8
+ Thanks for filing a report. For a **security** issue, do not use this
9
+ form — see [SECURITY.md](../security/policy) and report privately.
10
+ - type: input
11
+ id: version
12
+ attributes:
13
+ label: Version and environment
14
+ description: dbt-costgate version, dbt / dbt-bigquery versions, Python version, and where it ran.
15
+ placeholder: "dbt-costgate 0.1.0, dbt-core 1.9.x, dbt-bigquery 1.9.x, Python 3.12, GitHub Actions"
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: expected
20
+ attributes:
21
+ label: Expected behavior
22
+ description: What should dbt-costgate have reported or done? If a dollar/bytes figure is wrong, what does the BigQuery console's own dry-run estimate say for the same SQL?
23
+ validations:
24
+ required: true
25
+ - type: textarea
26
+ id: actual
27
+ attributes:
28
+ label: Actual behavior
29
+ description: What did it report or do? Paste the report output — **redact model names, dataset names, or figures you consider sensitive.**
30
+ validations:
31
+ required: true
32
+ - type: textarea
33
+ id: repro
34
+ attributes:
35
+ label: Reproduction
36
+ description: Minimal steps or command to reproduce. Do not include credentials or compiled SQL containing secrets.
37
+ render: shell
38
+ validations:
39
+ required: false
@@ -0,0 +1,8 @@
1
+ blank_issues_enabled: false
2
+ contact_links:
3
+ - name: Security vulnerability
4
+ url: https://github.com/Drichards124/dbt-costgate/security/advisories/new
5
+ about: Report security issues privately via GitHub's private vulnerability reporting — not as a public issue.
6
+ - name: Question or discussion
7
+ url: https://github.com/Drichards124/dbt-costgate/discussions
8
+ about: Ask usage questions or discuss ideas before filing a feature request.
@@ -0,0 +1,31 @@
1
+ name: Feature request
2
+ description: Propose an improvement to the gate, the report, or the pricing model.
3
+ labels: ["enhancement", "triage"]
4
+ body:
5
+ - type: textarea
6
+ id: problem
7
+ attributes:
8
+ label: Problem
9
+ description: What cost-visibility problem are you hitting that dbt-costgate doesn't solve today?
10
+ validations:
11
+ required: true
12
+ - type: textarea
13
+ id: proposal
14
+ attributes:
15
+ label: Proposed solution
16
+ description: What would you like dbt-costgate to do? If it affects reported dollars, say how the figure should be derived.
17
+ validations:
18
+ required: true
19
+ - type: textarea
20
+ id: alternatives
21
+ attributes:
22
+ label: Alternatives considered
23
+ description: Workarounds you use today, or other tools that partially cover this.
24
+ validations:
25
+ required: false
26
+ - type: markdown
27
+ attributes:
28
+ value: |
29
+ Note the project's hard invariants (see CONTRIBUTING.md): dry-run only,
30
+ no credential handling, no telemetry. Proposals that require breaking
31
+ one of these will be redirected to a design discussion.
@@ -0,0 +1,29 @@
1
+ <!--
2
+ Pull requests target the `ple` branch, not `main`. See CONTRIBUTING.md.
3
+ -->
4
+
5
+ ## What and why
6
+
7
+ <!-- What does this change do, and what problem does it solve? -->
8
+
9
+ ## Related issues
10
+
11
+ <!-- e.g. Closes #12 -->
12
+
13
+ ## Cost-safety invariants
14
+
15
+ If this touches warehouse interaction, pricing, or reporting, confirm:
16
+
17
+ - [ ] The only query type issued is a BigQuery **dry-run** (`dryRun=true`) — nothing billable, no table data read.
18
+ - [ ] No credential handling was added; auth still delegates entirely to Application Default Credentials.
19
+ - [ ] Reports still exclude compiled SQL by default (secrets can be templated into SQL).
20
+ - [ ] Pricing table changes cite Google's published pricing page and update `last_verified`.
21
+
22
+ ## Checklist
23
+
24
+ - [ ] Targets the `ple` branch.
25
+ - [ ] All commits are signed off for the DCO (`git commit -s`).
26
+ - [ ] Every new source file has the SPDX header (`SPDX-License-Identifier: Apache-2.0`).
27
+ - [ ] Tests pass locally (`python -m pytest -q`).
28
+ - [ ] Lint passes (`ruff check . && ruff format --check .`).
29
+ - [ ] `CHANGELOG.md` updated under `## [Unreleased]` if this is user-visible.
@@ -0,0 +1,19 @@
1
+ version: 2
2
+ updates:
3
+ # Runtime dependencies are deliberately minimal (see SECURITY.md — this tool
4
+ # runs next to warehouse credentials). Updates target `ple` so they pass the
5
+ # production-like gate before reaching `main`, like any change.
6
+ - package-ecosystem: "pip"
7
+ directory: "/"
8
+ target-branch: "ple"
9
+ schedule:
10
+ interval: "weekly"
11
+ open-pull-requests-limit: 5
12
+ labels: ["dependencies"]
13
+
14
+ - package-ecosystem: "github-actions"
15
+ directory: "/"
16
+ target-branch: "ple"
17
+ schedule:
18
+ interval: "weekly"
19
+ labels: ["dependencies", "ci"]
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Test double for the dbt-costgate CLI — used ONLY by action-selftest.yml to exercise
5
+ # action.yml's orchestration without BigQuery. Writes a canned markdown report to
6
+ # --output and exits with $SHIM_EXIT (default 0). On exit 2 it writes NOTHING,
7
+ # reproducing dbt-costgate's real "no report on operational error" behavior so the
8
+ # action's synthesized-body path is exercised.
9
+ out=""
10
+ while [ $# -gt 0 ]; do
11
+ case "$1" in
12
+ --output)
13
+ shift
14
+ out="$1"
15
+ ;;
16
+ esac
17
+ shift
18
+ done
19
+ code="${SHIM_EXIT:-0}"
20
+ if [ "$code" != "2" ] && [ -n "$out" ]; then
21
+ printf '### 💸 dbt-costgate — cost impact of this change\n\n_self-test report_\n' >"$out"
22
+ fi
23
+ exit "$code"
@@ -0,0 +1,40 @@
1
+ #!/usr/bin/env bash
2
+ # SPDX-License-Identifier: Apache-2.0
3
+ #
4
+ # Test double for the gh CLI — used ONLY by action-selftest.yml. The list call
5
+ # (recognised by --jq) prints $FAKE_GH_IDS; a POST/PATCH appends one tab-separated
6
+ # line (method, URL, whether the body carried the sticky marker) to $FAKE_GH_LOG.
7
+ # No network, no token.
8
+ method=""
9
+ url=""
10
+ body=""
11
+ is_list=0
12
+ args=("$@")
13
+ i=0
14
+ while [ $i -lt ${#args[@]} ]; do
15
+ a="${args[$i]}"
16
+ case "$a" in
17
+ --jq) is_list=1 ;;
18
+ --method)
19
+ i=$((i + 1))
20
+ method="${args[$i]}"
21
+ ;;
22
+ -F)
23
+ i=$((i + 1))
24
+ f="${args[$i]}"
25
+ case "$f" in body=@*) body="${f#body=@}" ;; esac
26
+ ;;
27
+ repos/*) url="$a" ;;
28
+ esac
29
+ i=$((i + 1))
30
+ done
31
+ if [ "$is_list" = "1" ]; then
32
+ [ -n "${FAKE_GH_IDS:-}" ] && printf '%s\n' "$FAKE_GH_IDS"
33
+ exit 0
34
+ fi
35
+ marker=no
36
+ if [ -n "$body" ] && [ -f "$body" ]; then
37
+ grep -qF '<!-- dbt-costgate-sticky -->' "$body" && marker=yes
38
+ fi
39
+ printf '%s\t%s\tmarker=%s\n' "$method" "$url" "$marker" >>"${FAKE_GH_LOG:-/dev/null}"
40
+ exit 0
@@ -0,0 +1,124 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ name: Action self-test
3
+
4
+ # Exercises action.yml end to end without BigQuery or the real GitHub API: a
5
+ # shimmed `dbt-costgate` drives each exit code (writing no report on exit 2, as the
6
+ # real CLI does) and a shimmed `gh` records the comment calls. Verifies the parts
7
+ # unit tests can't reach — input plumbing, exit-code propagation, and the
8
+ # synthesized-body path — while tests/test_sticky_comment.py covers the upsert.
9
+ on:
10
+ pull_request:
11
+ branches: [ple, main]
12
+ paths:
13
+ - action.yml
14
+ - scripts/**
15
+ - .github/selftest/**
16
+ - .github/workflows/action-selftest.yml
17
+ push:
18
+ branches-ignore: [main, ple]
19
+ paths:
20
+ - action.yml
21
+ - scripts/**
22
+ - .github/selftest/**
23
+ - .github/workflows/action-selftest.yml
24
+
25
+ permissions:
26
+ contents: read
27
+
28
+ concurrency:
29
+ group: action-selftest-${{ github.ref }}
30
+ cancel-in-progress: true
31
+
32
+ jobs:
33
+ selftest:
34
+ runs-on: ubuntu-latest
35
+ steps:
36
+ - uses: actions/checkout@v4
37
+ - uses: actions/setup-python@v5
38
+ with:
39
+ python-version: "3.12"
40
+
41
+ - name: Put the test doubles ahead of everything on PATH
42
+ run: |
43
+ chmod +x "$GITHUB_WORKSPACE/.github/selftest/dbt-costgate" "$GITHUB_WORKSPACE/.github/selftest/gh"
44
+ echo "$GITHUB_WORKSPACE/.github/selftest" >>"$GITHUB_PATH"
45
+
46
+ # --- Case A: pass, no existing sticky → POST, step succeeds ---
47
+ - name: Case A — pass
48
+ id: case_a
49
+ uses: ./
50
+ env:
51
+ SHIM_EXIT: "0"
52
+ FAKE_GH_IDS: ""
53
+ FAKE_GH_LOG: ${{ runner.temp }}/a.log
54
+ with:
55
+ comment: "true"
56
+ - name: Assert A outputs
57
+ run: |
58
+ [ "${{ steps.case_a.outputs.status }}" = "ok" ] || { echo "A: status != ok"; exit 1; }
59
+ [ "${{ steps.case_a.outputs.exit-code }}" = "0" ] || { echo "A: exit-code != 0"; exit 1; }
60
+ - name: Assert A comment
61
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
62
+ run: |
63
+ grep -qE '^POST[[:space:]].*marker=yes' "${{ runner.temp }}/a.log" \
64
+ || { echo "A: expected a POST carrying the marker"; cat "${{ runner.temp }}/a.log" 2>/dev/null; exit 1; }
65
+
66
+ # --- Case B: gate failed (exit 1), existing sticky → PATCH, step fails ---
67
+ - name: Case B — gate fail
68
+ id: case_b
69
+ continue-on-error: true
70
+ uses: ./
71
+ env:
72
+ SHIM_EXIT: "1"
73
+ FAKE_GH_IDS: "777"
74
+ FAKE_GH_LOG: ${{ runner.temp }}/b.log
75
+ with:
76
+ comment: "true"
77
+ - name: Assert B outputs
78
+ run: |
79
+ [ "${{ steps.case_b.outcome }}" = "failure" ] || { echo "B: expected step failure"; exit 1; }
80
+ [ "${{ steps.case_b.outputs.status }}" = "failed" ] || { echo "B: status != failed"; exit 1; }
81
+ [ "${{ steps.case_b.outputs.exit-code }}" = "1" ] || { echo "B: exit-code != 1"; exit 1; }
82
+ - name: Assert B comment
83
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
84
+ run: |
85
+ grep -qE '^PATCH[[:space:]]repos/.*/issues/comments/777' "${{ runner.temp }}/b.log" \
86
+ || { echo "B: expected a PATCH of comment 777"; cat "${{ runner.temp }}/b.log" 2>/dev/null; exit 1; }
87
+
88
+ # --- Case C: operational (exit 2), alert-only → synthesized body, step passes ---
89
+ - name: Case C — operational, alert-only
90
+ id: case_c
91
+ uses: ./
92
+ env:
93
+ SHIM_EXIT: "2"
94
+ FAKE_GH_IDS: ""
95
+ FAKE_GH_LOG: ${{ runner.temp }}/c.log
96
+ with:
97
+ comment: "true"
98
+ fail-on-operational: "false"
99
+ - name: Assert C outputs
100
+ run: |
101
+ [ "${{ steps.case_c.outcome }}" = "success" ] || { echo "C: expected step success (alert-only)"; exit 1; }
102
+ [ "${{ steps.case_c.outputs.status }}" = "error" ] || { echo "C: status != error"; exit 1; }
103
+ [ "${{ steps.case_c.outputs.exit-code }}" = "2" ] || { echo "C: exit-code != 2"; exit 1; }
104
+ - name: Assert C comment
105
+ if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.fork == false
106
+ run: |
107
+ grep -qE '^POST[[:space:]].*marker=yes' "${{ runner.temp }}/c.log" \
108
+ || { echo "C: expected a synthesized POST carrying the marker"; cat "${{ runner.temp }}/c.log" 2>/dev/null; exit 1; }
109
+
110
+ # --- Case D: operational (exit 2), fail-on-operational → step fails ---
111
+ - name: Case D — operational, hard-fail
112
+ id: case_d
113
+ continue-on-error: true
114
+ uses: ./
115
+ env:
116
+ SHIM_EXIT: "2"
117
+ FAKE_GH_LOG: ${{ runner.temp }}/d.log
118
+ with:
119
+ comment: "false"
120
+ fail-on-operational: "true"
121
+ - name: Assert D outputs
122
+ run: |
123
+ [ "${{ steps.case_d.outcome }}" = "failure" ] || { echo "D: expected step failure"; exit 1; }
124
+ [ "${{ steps.case_d.outputs.exit-code }}" = "2" ] || { echo "D: exit-code != 2"; exit 1; }
@@ -0,0 +1,49 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ name: CI
3
+
4
+ # Fast feedback on every pull request and every feature-branch push. The heavier
5
+ # production-like matrix runs separately once a change lands on `ple` (ple.yml).
6
+ on:
7
+ pull_request:
8
+ branches: [ple, main]
9
+ push:
10
+ branches-ignore: [main, ple]
11
+
12
+ permissions:
13
+ contents: read
14
+
15
+ concurrency:
16
+ group: ci-${{ github.ref }}
17
+ cancel-in-progress: true
18
+
19
+ jobs:
20
+ python:
21
+ name: Python (${{ matrix.python }})
22
+ runs-on: ubuntu-latest
23
+ strategy:
24
+ fail-fast: false
25
+ matrix:
26
+ python: ["3.9", "3.10", "3.11", "3.12", "3.13"]
27
+ steps:
28
+ - uses: actions/checkout@v4
29
+ - uses: actions/setup-python@v5
30
+ with:
31
+ python-version: ${{ matrix.python }}
32
+ - run: python -m pip install -e ".[dev]"
33
+ - run: ruff check .
34
+ - run: ruff format --check .
35
+ - run: python -m pytest -q
36
+
37
+ ci-gate:
38
+ name: CI gate
39
+ runs-on: ubuntu-latest
40
+ needs: [python]
41
+ if: always()
42
+ steps:
43
+ - name: Require all CI jobs to have succeeded
44
+ run: |
45
+ if [ "${{ needs.python.result }}" != "success" ]; then
46
+ echo "One or more CI jobs did not succeed."
47
+ exit 1
48
+ fi
49
+ echo "All CI checks passed."
@@ -0,0 +1,70 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ name: PLE
3
+
4
+ # Production-Like-Environment gate. Everything merges to `ple` before `main`.
5
+ # This proves the change on the full OS/runtime matrix AND against the
6
+ # actually-packaged artifact (install the built wheel fresh, then run it) —
7
+ # not just against the source tree. A green PLE run is the precondition for
8
+ # promoting `ple` to `main` and cutting a release.
9
+ on:
10
+ push:
11
+ branches: [ple]
12
+ workflow_dispatch:
13
+
14
+ permissions:
15
+ contents: read
16
+
17
+ concurrency:
18
+ group: ple-${{ github.ref }}
19
+ cancel-in-progress: false
20
+
21
+ jobs:
22
+ py-matrix:
23
+ name: Tests (${{ matrix.os }} / ${{ matrix.python }})
24
+ runs-on: ${{ matrix.os }}
25
+ strategy:
26
+ fail-fast: false
27
+ matrix:
28
+ os: [ubuntu-latest, macos-latest, windows-latest]
29
+ python: ["3.9", "3.13"]
30
+ steps:
31
+ - uses: actions/checkout@v4
32
+ - uses: actions/setup-python@v5
33
+ with:
34
+ python-version: ${{ matrix.python }}
35
+ - run: python -m pip install -e ".[dev]"
36
+ - run: ruff check .
37
+ - run: python -m pytest -q
38
+
39
+ packaged-artifact-smoke:
40
+ name: Packaged-artifact smoke test
41
+ runs-on: ubuntu-latest
42
+ steps:
43
+ - uses: actions/checkout@v4
44
+ - uses: actions/setup-python@v5
45
+ with:
46
+ python-version: "3.12"
47
+ - name: Build wheel and sdist
48
+ run: |
49
+ python -m pip install build
50
+ python -m build
51
+ - name: Install the built wheel into a clean environment and run it
52
+ run: |
53
+ python -m venv /tmp/smoke
54
+ /tmp/smoke/bin/pip install dist/*.whl
55
+ /tmp/smoke/bin/dbt-costgate --version
56
+ /tmp/smoke/bin/python -c "import dbt_costgate; print(dbt_costgate.__version__)"
57
+
58
+ ple-gate:
59
+ name: PLE gate
60
+ runs-on: ubuntu-latest
61
+ needs: [py-matrix, packaged-artifact-smoke]
62
+ if: always()
63
+ steps:
64
+ - name: Require all PLE jobs to have succeeded
65
+ run: |
66
+ if [ "${{ needs.py-matrix.result }}" != "success" ] || [ "${{ needs.packaged-artifact-smoke.result }}" != "success" ]; then
67
+ echo "One or more PLE jobs did not succeed."
68
+ exit 1
69
+ fi
70
+ echo "PLE matrix green — safe to promote ple to main."
@@ -0,0 +1,108 @@
1
+ # SPDX-License-Identifier: Apache-2.0
2
+ name: Release
3
+
4
+ # Cut a GitHub Release when a vX.Y.Z tag is pushed to main. Builds the wheel and
5
+ # sdist, checksums them, smoke-tests the built wheel, and publishes a Release with
6
+ # those artifacts and the CHANGELOG section for the version. No PyPI publish.
7
+ on:
8
+ push:
9
+ tags: ["v[0-9]+.[0-9]+.[0-9]+"]
10
+
11
+ permissions:
12
+ contents: write # only to create the GitHub Release
13
+
14
+ jobs:
15
+ release:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@v4
19
+ - uses: actions/setup-python@v5
20
+ with:
21
+ python-version: "3.12"
22
+
23
+ - name: Verify tag matches package version
24
+ run: |
25
+ version="${GITHUB_REF_NAME#v}"
26
+ pkg="$(python -c "import re, pathlib; print(re.search(r'__version__ = \"([^\"]+)\"', pathlib.Path('src/dbt_costgate/__init__.py').read_text()).group(1))")"
27
+ if [ "$version" != "$pkg" ]; then
28
+ echo "Tag $GITHUB_REF_NAME (version $version) does not match package version $pkg."
29
+ exit 1
30
+ fi
31
+ echo "Tag matches package version $pkg."
32
+
33
+ - name: Build wheel and sdist
34
+ run: |
35
+ python -m pip install build
36
+ python -m build
37
+
38
+ - name: Checksum the artifacts
39
+ run: |
40
+ ( cd dist && sha256sum *.whl *.tar.gz > SHA256SUMS )
41
+ cat dist/SHA256SUMS
42
+
43
+ - name: Install the built wheel into a clean environment and verify it
44
+ run: |
45
+ version="${GITHUB_REF_NAME#v}"
46
+ python -m venv /tmp/smoke
47
+ /tmp/smoke/bin/pip install dist/*.whl
48
+ /tmp/smoke/bin/dbt-costgate --version
49
+ got="$(/tmp/smoke/bin/python -c "import dbt_costgate; print(dbt_costgate.__version__)")"
50
+ if [ "$got" != "$version" ]; then
51
+ echo "Installed wheel reports version $got, expected $version."
52
+ exit 1
53
+ fi
54
+
55
+ - name: Extract release notes from CHANGELOG
56
+ run: |
57
+ version="${GITHUB_REF_NAME#v}"
58
+ awk -v ver="$version" '
59
+ index($0, "## [" ver "]") == 1 { flag=1; next }
60
+ /^## \[/ { flag=0 }
61
+ flag { print }
62
+ ' CHANGELOG.md > RELEASE_NOTES.md
63
+ [ -s RELEASE_NOTES.md ] || { echo "no CHANGELOG section for $version"; exit 1; }
64
+ cat RELEASE_NOTES.md
65
+
66
+ - name: Create GitHub Release
67
+ env:
68
+ GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
69
+ run: |
70
+ gh release create "$GITHUB_REF_NAME" dist/* \
71
+ --title "dbt-costgate $GITHUB_REF_NAME" \
72
+ --notes-file RELEASE_NOTES.md
73
+
74
+ # Hand the distributions to the publish job. SHA256SUMS is deliberately not
75
+ # included: PyPI rejects anything in the upload directory that isn't a
76
+ # distribution, and it belongs to the GitHub Release, not to PyPI.
77
+ - name: Upload distributions for the PyPI job
78
+ uses: actions/upload-artifact@v4
79
+ with:
80
+ name: dist
81
+ path: |
82
+ dist/*.whl
83
+ dist/*.tar.gz
84
+ if-no-files-found: error
85
+
86
+ # Publishing runs only after the GitHub Release exists, so a PyPI outage leaves
87
+ # a complete, downloadable release behind rather than nothing at all.
88
+ pypi:
89
+ name: Publish to PyPI
90
+ needs: release
91
+ runs-on: ubuntu-latest
92
+ environment: pypi
93
+ # Trusted publishing (OIDC): GitHub mints a short-lived identity token that
94
+ # PyPI verifies against the registered publisher. There is no API token to
95
+ # store, rotate, or leak. Declared here rather than at the top of the file so
96
+ # only this job can request it — and note that a job-level block replaces the
97
+ # workflow default, so this job deliberately has no `contents: write`.
98
+ permissions:
99
+ id-token: write
100
+ steps:
101
+ - name: Download the distributions built above
102
+ uses: actions/download-artifact@v4
103
+ with:
104
+ name: dist
105
+ path: dist
106
+
107
+ - name: Publish to PyPI
108
+ uses: pypa/gh-action-pypi-publish@release/v1
@@ -0,0 +1,31 @@
1
+ # --- General ---
2
+ .DS_Store
3
+ *.log
4
+ .env
5
+ .env.*
6
+ !.env.example
7
+
8
+ # --- Python ---
9
+ __pycache__/
10
+ *.py[cod]
11
+ .pytest_cache/
12
+ .mypy_cache/
13
+ .ruff_cache/
14
+ .venv/
15
+ venv/
16
+ dist/
17
+ build/
18
+ *.egg-info/
19
+ coverage/
20
+ .coverage
21
+
22
+ # --- Editors ---
23
+ .idea/
24
+ .vscode/
25
+ *.swp
26
+
27
+ # --- Local working notes (private; not released) ---
28
+ # Decision records are personal reasoning that feeds docs/architecture.md.
29
+ # Only architecture.md and its upgrades ship publicly.
30
+ docs/decisions/
31
+ CLAUDE.local.md