Repository navigation
230 lines (215 loc) · 10.4 KB
/
Copy pathrelease.yml
File metadata and controls
230 lines (215 loc) · 10.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
# Build the CAS BACnet Stack library, smoke-test and Wire-Test the B-SS Python example, and
# publish a GitHub Release (the example's source zip + metrics.json - the runnable app, without
# the stack library) when a vX.Y.Z tag is pushed.
# PRs run the same build and tests without publishing.
#
# Unlike the C++/C# siblings, there is no language build step here at all: Python is
# interpreted, so `main.py` runs directly - `pip install` of anything beyond the standard
# library is NOT required (see README.md "Prerequisites"). The only compiled artifact is the
# CAS BACnet Stack shared library, built from the stack submodule's C++ source into the
# submodule's own bin/ folder with the stack's CMake, its only build (the windows-x64-dll
# preset on Windows, linux-x64-release-shared on Linux, then `cmake --install` into bin/ - see
# README.md "Build the CAS BACnet Stack library"), where main.py loads it with ctypes. main.py uses the stack's generated Python adapter in place from
# submodules/cas-bacnet-stack/adapters/python/.
#
# THE STACK BINARY IS NEVER PACKAGED. The CAS BACnet Stack (library and adapter) is a
# separate, commercially licensed product: CI builds the library only to test this example,
# and neither it nor the adapter goes into any uploaded artifact or release asset. A user
# builds the library from the submodule (README.md). The Package step below checks this, and
# the next step runs the packaged app without the library: it must print the series'
# missing-library message and exit 1.
#
# The CAS BACnet Stack is a PRIVATE git submodule: the repository secret CAS_STACK_PAT must
# hold a PAT with read access to chipkin/cas-bacnet-stack.
name: build-and-release
on:
pull_request:
push:
tags: ["v*.*.*"]
workflow_dispatch:
permissions:
contents: write
jobs:
build:
name: build (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
# windows-2025 serves the VS 2026 image (v145), which the stack's windows-x64-dll
# preset generates for and the stack's own release.yml builds its DLL on.
- os: windows-2025
artifact: BACnetProfileExampleBSSPython-windows-x64
nativelib: CASBACnetStack_x64_Release.dll
metrics: metrics-windows.json
- os: ubuntu-latest
artifact: BACnetProfileExampleBSSPython-linux-x64
nativelib: libCASBACnetStack_x64_Release.so
metrics: metrics-linux.json
steps:
- name: Enable git long paths (Windows)
if: runner.os == 'Windows'
run: git config --system core.longpaths true
- name: Checkout (with the CAS BACnet Stack submodule)
uses: actions/checkout@v4
with:
submodules: recursive
token: ${{ secrets.CAS_STACK_PAT || github.token }}
- name: Stack commit (recorded in metrics.json)
id: stack
shell: bash
run: echo "sha=$(git -C submodules/cas-bacnet-stack rev-parse HEAD)" >> "$GITHUB_OUTPUT"
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.x"
- name: Compile-check main.py, common/ and tests/ (no pip install needed - stdlib only)
shell: bash
run: python -m py_compile main.py common/*.py tests/*.py
- name: Build the CAS BACnet Stack library (Windows - CMake preset windows-x64-dll)
if: runner.os == 'Windows'
shell: pwsh
working-directory: submodules/cas-bacnet-stack
run: |
cmake --preset windows-x64-dll
if ($LASTEXITCODE -ne 0) { throw "windows-x64-dll configure failed" }
cmake --build --preset windows-x64-dll-release
if ($LASTEXITCODE -ne 0) { throw "windows-x64-dll build failed" }
cmake --install build-windows-x64-dll --config Release
if ($LASTEXITCODE -ne 0) { throw "windows-x64-dll install failed" }
Get-ChildItem bin\CASBACnetStack_x64_Release.dll
- name: Build the CAS BACnet Stack library (Linux - CMake preset linux-x64-release-shared)
if: runner.os == 'Linux'
shell: bash
working-directory: submodules/cas-bacnet-stack
run: |
set -e
sudo apt-get install -y ninja-build
cmake --preset linux-x64-release-shared
cmake --build --preset linux-x64-release-shared
cmake --install build-linux-x64-release-shared
ls -la bin/libCASBACnetStack_x64_Release.so
- name: Smoke test + metrics (starts, binds, stays up; measures time to 'ready')
shell: bash
run: |
start=$(date +%s%3N)
python main.py --port 47822 > smoke.log 2>&1 &
PID=$!
ready_ms=""
for i in $(seq 1 100); do
if grep -q "ready" smoke.log; then ready_ms=$(( $(date +%s%3N) - start )); break; fi
sleep 0.1
done
sleep 2
if kill -0 "$PID" 2>/dev/null; then kill "$PID" 2>/dev/null || true; else echo "FAIL: device exited early"; cat smoke.log; exit 1; fi
[ -n "$ready_ms" ] || { echo "FAIL: no 'ready' line"; cat smoke.log; exit 1; }
cat smoke.log
lib="submodules/cas-bacnet-stack/bin/${{ matrix.nativelib }}"
bytes=$(stat -c %s "$lib" 2>/dev/null || wc -c < "$lib")
sha=$(sha256sum "$lib" | cut -c1-16)
stackver=$(grep -oE 'CAS BACnet Stack version: [0-9.]+' smoke.log | head -1 | grep -oE '[0-9.]+$' || true)
appver=$(grep -oE 'v[0-9]+\.[0-9]+\.[0-9]+' smoke.log | head -1 || true)
cat > "${{ matrix.metrics }}" <<EOF
{
"example": "BACnetProfileExampleBSSPython",
"app_version": "$appver",
"os": "${{ matrix.os }}",
"link_mode": "ctypes-CDLL",
"stack_commit": "${{ steps.stack.outputs.sha }}",
"stack_version": "$stackver",
"nativelib_bytes": $bytes,
"nativelib_sha256_prefix": "$sha",
"startup_ms_to_ready": $ready_ms,
"python_version": "$(python --version 2>&1 | cut -d' ' -f2)",
"built_at": "$(date -u +%Y-%m-%dT%H:%M:%SZ)",
"git_ref": "${{ github.ref_name }}"
}
EOF
cat "${{ matrix.metrics }}"
{ echo '### metrics (${{ matrix.os }})'; echo '```json'; cat "${{ matrix.metrics }}"; echo '```'; } >> "$GITHUB_STEP_SUMMARY"
# The Wire Tests: a Python client (the Peer) discovers the sensor, reads it following
# the sensor Simulation, and drives its Failure Modes (tests/README.md). Python 3
# standard library only.
- name: Wire Tests
if: runner.os == 'Linux'
shell: bash
run: python tests/run_wire_tests.py --exe main.py
- name: Package (the example's own files - never the stack library or adapter)
shell: bash
run: |
mkdir -p dist
cp "${{ matrix.metrics }}" dist/
# The source and docs ship once, from the Linux build, as one zip (folders keep
# their layout): the Windows checkout converts them to CRLF, so the two platforms'
# copies would differ byte for byte and the release job's one-copy-per-name check
# would refuse them.
if [ "$RUNNER_OS" = "Linux" ]; then
zip -r dist/BACnetProfileExampleBSSPython.zip main.py common tests wiretest docs \
README.md TUTORIAL.md CHANGELOG.md LICENSE -x '*__pycache__*'
cp README.md LICENSE dist/
fi
# The CAS BACnet Stack is never redistributed: refuse any stack library or addon
# (CASBACnetStack* / libCASBACnetStack* / *.node), loose or inside the zip.
if { find dist -type f; unzip -Z1 dist/*.zip 2>/dev/null; } | grep -iE '(^|/)(lib)?CASBACnetStack[^/]*$|\.node$'; then
echo "FAIL: a CAS BACnet Stack library is in dist/"; exit 1
fi
# Owner ruling 2b: the release is the app without the stack library, and it must say
# so clearly. Run the packaged app, unzipped on its own with no stack anywhere, and
# check the first line of its message and its exit code.
- name: The packaged app without the stack library says so and exits 1
if: runner.os == 'Linux'
shell: bash
run: |
rm -rf "$RUNNER_TEMP/packaged" && mkdir -p "$RUNNER_TEMP/packaged"
unzip -q dist/BACnetProfileExampleBSSPython.zip -d "$RUNNER_TEMP/packaged"
cd "$RUNNER_TEMP/packaged"
set +e
python main.py --port 47823 > stdout.txt 2> stderr.txt
code=$?
set -e
cat stdout.txt stderr.txt
[ "$code" = "1" ] || { echo "FAIL: exit code $code, want 1"; exit 1; }
first=$(head -n 1 stderr.txt | tr -d '\r')
[ "$first" = "ERROR: This example cannot run without the CAS BACnet Stack library." ] \
|| { echo "FAIL: first line was: $first"; exit 1; }
python main.py --version # works without the library
- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: ${{ matrix.artifact }}
path: dist/*
release:
name: publish release
needs: build
if: startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-latest
steps:
- uses: actions/download-artifact@v4
with:
path: artifacts
# Every platform artifact could carry a file of the same name. Uploading two assets
# with one name makes the release action fail part-way and leaves a draft release
# behind (a real tagged release hit this). Flatten the artifacts into one folder with
# a single copy of each file name, and stop if two files share a name but differ.
- name: Collect release assets (one copy of each file name)
shell: bash
run: |
mkdir -p release
find artifacts -type f | sort | while read -r f; do
name=$(basename "$f")
if [ -e "release/$name" ]; then
cmp -s "$f" "release/$name" || { echo "FAIL: two different files are both named $name ($f)"; exit 1; }
else
cp "$f" "release/$name"
fi
done
if { find release -type f; unzip -Z1 release/*.zip 2>/dev/null; } | grep -iE '(^|/)(lib)?CASBACnetStack[^/]*$|\.node$'; then
echo "FAIL: a CAS BACnet Stack library is in the release assets"; exit 1
fi
ls -l release
- uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
generate_release_notes: true
files: release/*