Skip to content

Commit 871b8f9

Browse files
TheTrustedAdvisoredkreukCopilot
authored
CI: fail the build when the committed dacpac does not match the SQL sources (#263)
* CI: fail the build when the committed dacpac does not match the SQL sources src/SQL_FMD_FRAMEWORK.SQLDatabase/ contains nothing but the dacpac, and that is what deploy_item imports. The .sql files under src/Config_Database/ are the sources of SQL_FMD_FRAMEWORK.sqlproj, which builds it. So a change to a .sql file reaches nobody's database until the dacpac is rebuilt, and nothing in the repository says so. This workflow rebuilds the project on any change under src/Config_Database/ and compares model.xml from the built dacpac against model.xml from the committed one. If they differ it fails, prints which element types drifted, and gives the two commands that fix it. It compares model.xml rather than the whole file because Origin.xml carries build checksums and DacMetadata a version, so a byte comparison would fail on every build. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Erwin de Kreuk <erwin.dekreuk@gmail.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
1 parent 9bfb0ba commit 871b8f9

1 file changed

Lines changed: 97 additions & 0 deletions

File tree

‎.github/workflows/dacpac-guard.yml‎

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
name: dacpac is up to date
2+
3+
# The SQLDatabase item folder contains nothing but SQL_FMD_FRAMEWORK.dacpac, and
4+
# that is what NB_SETUP_FMD deploys. The .sql files under src/Config_Database/ are
5+
# the sources of SQL_FMD_FRAMEWORK.sqlproj, which builds it.
6+
#
7+
# So a change to a .sql file reaches nobody's database until the dacpac is rebuilt.
8+
# This job rebuilds it and fails if the committed artefact does not match, which
9+
# makes that mistake impossible to merge.
10+
11+
on:
12+
push:
13+
branches: [main]
14+
paths:
15+
- "src/Config_Database/**"
16+
- "src/SQL_FMD_FRAMEWORK.SQLDatabase/**"
17+
- ".github/workflows/dacpac-guard.yml"
18+
pull_request:
19+
paths:
20+
- "src/Config_Database/**"
21+
- "src/SQL_FMD_FRAMEWORK.SQLDatabase/**"
22+
- ".github/workflows/dacpac-guard.yml"
23+
workflow_dispatch:
24+
25+
jobs:
26+
check:
27+
runs-on: ubuntu-latest
28+
steps:
29+
- uses: actions/checkout@v4
30+
31+
- uses: actions/setup-dotnet@v4
32+
with:
33+
dotnet-version: "8.0.x"
34+
35+
- name: Build the SQL project
36+
run: dotnet build src/Config_Database/SQL_FMD_FRAMEWORK.sqlproj -c Release
37+
38+
- name: Compare the built model against the committed dacpac
39+
shell: python
40+
run: |
41+
import zipfile, sys, difflib
42+
43+
BUILT = "src/Config_Database/bin/Release/SQL_FMD_FRAMEWORK.dacpac"
44+
COMMITTED = "src/SQL_FMD_FRAMEWORK.SQLDatabase/SQL_FMD_FRAMEWORK.dacpac"
45+
46+
def model(path):
47+
# model.xml is the resolved schema. Origin.xml carries build checksums and
48+
# DacMetadata a version, so comparing whole files would fail on every build.
49+
return zipfile.ZipFile(path).read("model.xml").decode("utf-8")
50+
51+
built, committed = model(BUILT), model(COMMITTED)
52+
53+
if built == committed:
54+
print("The committed dacpac matches the SQL sources.")
55+
sys.exit(0)
56+
57+
print("::error::The committed dacpac does NOT match src/Config_Database/.")
58+
print("")
59+
print("The dacpac is the only thing that deploys: src/SQL_FMD_FRAMEWORK.SQLDatabase/")
60+
print("contains nothing else, and deploy_item imports that folder. So this change")
61+
print("does not reach anybody's database until the dacpac is rebuilt.")
62+
print("")
63+
print("Rebuild it with:")
64+
print("")
65+
print(" dotnet build src/Config_Database/SQL_FMD_FRAMEWORK.sqlproj -c Release")
66+
print(" cp src/Config_Database/bin/Release/SQL_FMD_FRAMEWORK.dacpac \\")
67+
print(" src/SQL_FMD_FRAMEWORK.SQLDatabase/SQL_FMD_FRAMEWORK.dacpac")
68+
print("")
69+
70+
# Say WHAT differs, not just that something does. Counting the model's own
71+
# element types is enough to name the drift in one line.
72+
def summarise(x):
73+
return {t: x.count(f'Type="{t}"') for t in
74+
("SqlTable", "SqlProcedure", "SqlView", "SqlUser",
75+
"SqlSchema", "SqlRoleMembership", "SqlPermissionStatement",
76+
"SqlSimpleColumn", "SqlIndex", "SqlPrimaryKeyConstraint")}
77+
78+
a, b = summarise(committed), summarise(built)
79+
drift = [(k, a[k], b[k]) for k in a if a[k] != b[k]]
80+
if drift:
81+
print("What changed (committed -> built):")
82+
for k, old, new in drift:
83+
print(f" {k:26} {old:4} -> {new:4}")
84+
else:
85+
print("Element counts match; the difference is in the object bodies.")
86+
87+
# A short unified diff of the first divergence, so the log is actionable.
88+
d = list(difflib.unified_diff(
89+
committed.splitlines(), built.splitlines(),
90+
fromfile="committed dacpac", tofile="built from sources", lineterm="", n=1))
91+
if d:
92+
print("")
93+
print("First differences:")
94+
for line in d[:40]:
95+
print(" " + line[:160])
96+
97+
sys.exit(1)

0 commit comments

Comments
 (0)