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.
- dbt_costgate-0.7.1/.editorconfig +18 -0
- dbt_costgate-0.7.1/.github/ISSUE_TEMPLATE/bug_report.yml +39 -0
- dbt_costgate-0.7.1/.github/ISSUE_TEMPLATE/config.yml +8 -0
- dbt_costgate-0.7.1/.github/ISSUE_TEMPLATE/feature_request.yml +31 -0
- dbt_costgate-0.7.1/.github/PULL_REQUEST_TEMPLATE.md +29 -0
- dbt_costgate-0.7.1/.github/dependabot.yml +19 -0
- dbt_costgate-0.7.1/.github/selftest/dbt-costgate +23 -0
- dbt_costgate-0.7.1/.github/selftest/gh +40 -0
- dbt_costgate-0.7.1/.github/workflows/action-selftest.yml +124 -0
- dbt_costgate-0.7.1/.github/workflows/ci.yml +49 -0
- dbt_costgate-0.7.1/.github/workflows/ple.yml +70 -0
- dbt_costgate-0.7.1/.github/workflows/release.yml +108 -0
- dbt_costgate-0.7.1/.gitignore +31 -0
- dbt_costgate-0.7.1/CHANGELOG.md +181 -0
- dbt_costgate-0.7.1/CODEOWNERS +12 -0
- dbt_costgate-0.7.1/CODE_OF_CONDUCT.md +132 -0
- dbt_costgate-0.7.1/CONTRIBUTING.md +96 -0
- dbt_costgate-0.7.1/LICENSE +201 -0
- dbt_costgate-0.7.1/NOTICE +25 -0
- dbt_costgate-0.7.1/PKG-INFO +218 -0
- dbt_costgate-0.7.1/README.md +191 -0
- dbt_costgate-0.7.1/SECURITY.md +47 -0
- dbt_costgate-0.7.1/action.yml +205 -0
- dbt_costgate-0.7.1/docs/architecture.md +103 -0
- dbt_costgate-0.7.1/docs/assets/flow.svg +60 -0
- dbt_costgate-0.7.1/docs/assets/logo.svg +16 -0
- dbt_costgate-0.7.1/docs/assets/pr-comment-mock.svg +70 -0
- dbt_costgate-0.7.1/docs/usage.md +387 -0
- dbt_costgate-0.7.1/pyproject.toml +54 -0
- dbt_costgate-0.7.1/scripts/sticky_comment.sh +55 -0
- dbt_costgate-0.7.1/src/dbt_costgate/__init__.py +4 -0
- dbt_costgate-0.7.1/src/dbt_costgate/against.py +150 -0
- dbt_costgate-0.7.1/src/dbt_costgate/artifacts.py +279 -0
- dbt_costgate-0.7.1/src/dbt_costgate/bigquery.py +110 -0
- dbt_costgate-0.7.1/src/dbt_costgate/cli.py +394 -0
- dbt_costgate-0.7.1/src/dbt_costgate/config.py +291 -0
- dbt_costgate-0.7.1/src/dbt_costgate/data/__init__.py +3 -0
- dbt_costgate-0.7.1/src/dbt_costgate/data/pricing.json +12 -0
- dbt_costgate-0.7.1/src/dbt_costgate/estimate.py +185 -0
- dbt_costgate-0.7.1/src/dbt_costgate/gitdiff.py +75 -0
- dbt_costgate-0.7.1/src/dbt_costgate/models.py +180 -0
- dbt_costgate-0.7.1/src/dbt_costgate/policy.py +63 -0
- dbt_costgate-0.7.1/src/dbt_costgate/pricing.py +94 -0
- dbt_costgate-0.7.1/src/dbt_costgate/report.py +252 -0
- dbt_costgate-0.7.1/tests/conftest.py +128 -0
- dbt_costgate-0.7.1/tests/test_against.py +248 -0
- dbt_costgate-0.7.1/tests/test_artifacts.py +243 -0
- dbt_costgate-0.7.1/tests/test_bigquery.py +50 -0
- dbt_costgate-0.7.1/tests/test_cli.py +400 -0
- dbt_costgate-0.7.1/tests/test_config.py +167 -0
- dbt_costgate-0.7.1/tests/test_estimate.py +111 -0
- dbt_costgate-0.7.1/tests/test_gitdiff.py +82 -0
- dbt_costgate-0.7.1/tests/test_policy.py +126 -0
- dbt_costgate-0.7.1/tests/test_pricing.py +82 -0
- dbt_costgate-0.7.1/tests/test_report.py +153 -0
- dbt_costgate-0.7.1/tests/test_scaffold.py +21 -0
- 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
|