release.yml 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. name: Build and publish Python package
  2. # Least-privilege default for every job; the publish job grants itself id-token below.
  3. permissions:
  4. contents: read
  5. on:
  6. pull_request:
  7. paths:
  8. - '.github/workflows/release.yml'
  9. - 'pyproject.toml'
  10. - 'README.md'
  11. - 'LICENSE.txt'
  12. - 'base_requirements.txt'
  13. - 'requirements.txt'
  14. - 'upgrade.sh'
  15. - 'contrib/**'
  16. - 'docs/**'
  17. - 'mkdocs.yml'
  18. - 'netbox/**'
  19. - 'scripts/packaging/**'
  20. - 'scripts/verify_*.py'
  21. - 'scripts/smoketest_configuration.py'
  22. push:
  23. tags:
  24. - 'v*'
  25. workflow_dispatch:
  26. jobs:
  27. build:
  28. name: Build package artifacts
  29. runs-on: ubuntu-latest
  30. # Match the validator versions bundled by the pinned publishing action.
  31. env:
  32. EXPECTED_TWINE_VERSION: '7.0.0'
  33. EXPECTED_PACKAGING_VERSION: '26.2'
  34. steps:
  35. - name: Check out repository
  36. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  37. with:
  38. persist-credentials: false
  39. - name: Set up Python
  40. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  41. with:
  42. python-version: '3.12'
  43. cache: pip
  44. - name: Install build tooling
  45. run: >-
  46. python -m pip install --upgrade
  47. build
  48. "twine==$EXPECTED_TWINE_VERSION"
  49. "packaging==$EXPECTED_PACKAGING_VERSION"
  50. - name: Install documentation toolchain
  51. run: python -m pip install -r requirements.txt
  52. - name: Verify pre-publication tool versions
  53. # Assert after all installation steps so twine check uses the expected
  54. # validator, and reject any incompatible shared dependency constraints.
  55. run: |
  56. python - <<'PY'
  57. import os
  58. from importlib.metadata import version
  59. expected = {
  60. 'twine': os.environ['EXPECTED_TWINE_VERSION'],
  61. 'packaging': os.environ['EXPECTED_PACKAGING_VERSION'],
  62. }
  63. for package, expected_version in expected.items():
  64. installed_version = version(package)
  65. print(f'{package}=={installed_version}')
  66. if installed_version != expected_version:
  67. raise SystemExit(f'{package}=={installed_version} is installed, expected {expected_version}')
  68. print(f'build=={version("build")}')
  69. PY
  70. python -m pip check
  71. - name: Render the documentation
  72. # -c = clean cache, -s = strict (abort on warnings); verify_wheel_contents.py
  73. # additionally guards against a partial render reaching the wheel.
  74. run: zensical build -c -s
  75. - name: Build sdist and wheel
  76. run: python -m build
  77. - name: Check package metadata
  78. run: twine check dist/*
  79. - name: Upload package artifacts
  80. uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
  81. with:
  82. name: python-package-distributions
  83. path: dist/
  84. if-no-files-found: error
  85. verify-dependencies:
  86. name: Verify dependency pins are in sync
  87. runs-on: ubuntu-latest
  88. needs: build
  89. steps:
  90. - name: Check out repository
  91. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  92. with:
  93. persist-credentials: false
  94. - name: Set up Python
  95. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  96. with:
  97. python-version: '3.12'
  98. cache: pip
  99. - name: Install packaging
  100. run: python -m pip install packaging
  101. - name: Verify requirements.txt is consistent with base_requirements.txt
  102. run: python scripts/verify_dependencies.py
  103. - name: Download package artifacts
  104. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  105. with:
  106. name: python-package-distributions
  107. path: dist/
  108. - name: Verify wheel Requires-Dist matches requirements.txt
  109. run: python scripts/verify_wheel_metadata.py dist/*.whl
  110. - name: Verify wheel excludes live configuration files
  111. run: python scripts/verify_wheel_contents.py dist/*.whl
  112. verify-sdist:
  113. name: Verify the sdist builds a wheel
  114. runs-on: ubuntu-latest
  115. needs: build
  116. steps:
  117. - name: Check out repository
  118. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  119. with:
  120. persist-credentials: false
  121. - name: Set up Python
  122. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  123. with:
  124. python-version: '3.12'
  125. cache: pip
  126. - name: Install tooling
  127. run: python -m pip install --upgrade pip packaging
  128. - name: Download package artifacts
  129. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  130. with:
  131. name: python-package-distributions
  132. path: dist/
  133. - name: Verify the sdist contents
  134. run: |
  135. python scripts/verify_sdist_contents.py dist/*.tar.gz
  136. - name: Build a wheel from the sdist
  137. run: |
  138. python -m pip wheel --no-deps dist/*.tar.gz -w sdist-wheel/
  139. - name: Verify the sdist-built wheel
  140. run: |
  141. python scripts/verify_wheel_metadata.py sdist-wheel/*.whl
  142. python scripts/verify_wheel_contents.py sdist-wheel/*.whl
  143. cli-smoke-test:
  144. name: Smoke test wheel CLI (no dependencies)
  145. runs-on: ubuntu-latest
  146. needs: build
  147. # The pre-configuration CLI paths are stdlib-only, so a --no-deps install suffices.
  148. # Unlike smoke-test, this job also runs on pull requests.
  149. steps:
  150. - name: Set up Python
  151. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  152. with:
  153. python-version: '3.12'
  154. - name: Download package artifacts
  155. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  156. with:
  157. name: python-package-distributions
  158. path: dist/
  159. - name: Install wheel without dependencies
  160. run: |
  161. python -m venv "$RUNNER_TEMP/netbox-cli-venv"
  162. "$RUNNER_TEMP/netbox-cli-venv/bin/python" -m pip install --no-deps dist/*.whl
  163. - name: Exercise the pre-configuration CLI
  164. run: |
  165. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" --version
  166. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" version
  167. "$RUNNER_TEMP/netbox-cli-venv/bin/python" -m netbox --version
  168. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" secret-key | grep -Eq '^.{50}$' || { echo "secret-key not 50 chars"; exit 1; }
  169. - name: Smoke-test netbox setup from the wheel
  170. run: |
  171. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" setup --target "$RUNNER_TEMP/nbroot"
  172. for f in "$RUNNER_TEMP/nbroot/conf/__init__.py" "$RUNNER_TEMP/nbroot/conf/configuration.py" "$RUNNER_TEMP/nbroot/local_requirements.txt"; do
  173. test -f "$f" || { echo "missing $f"; exit 1; }
  174. done
  175. for f in apache.conf gunicorn.py netbox-rq.service netbox.env netbox.service nginx.conf uwsgi.ini; do
  176. test -s "$RUNNER_TEMP/nbroot/contrib/$f" || { echo "missing or empty contrib/$f"; exit 1; }
  177. done
  178. smoke-test:
  179. name: Smoke test wheel install
  180. runs-on: ubuntu-latest
  181. needs: build
  182. # The wheel install + database migration is expensive; only run it for tag
  183. # pushes and manual dispatch, not on every packaging-related pull request.
  184. # cli-smoke-test provides lightweight, dependency-free CLI coverage on every PR instead.
  185. if: github.event_name != 'pull_request'
  186. services:
  187. postgres:
  188. image: postgres:17
  189. env:
  190. POSTGRES_DB: netbox
  191. POSTGRES_USER: netbox
  192. POSTGRES_PASSWORD: netbox
  193. ports:
  194. - 5432:5432
  195. options: >-
  196. --health-cmd "pg_isready -U netbox -d netbox"
  197. --health-interval 10s
  198. --health-timeout 5s
  199. --health-retries 5
  200. redis:
  201. image: redis:7
  202. ports:
  203. - 6379:6379
  204. options: >-
  205. --health-cmd "redis-cli ping"
  206. --health-interval 10s
  207. --health-timeout 5s
  208. --health-retries 5
  209. env:
  210. NETBOX_CONFIGURATION: smoketest_configuration
  211. POSTGRES_DB: netbox
  212. POSTGRES_USER: netbox
  213. POSTGRES_PASSWORD: netbox
  214. POSTGRES_HOST: 127.0.0.1
  215. POSTGRES_PORT: 5432
  216. REDIS_HOST: 127.0.0.1
  217. REDIS_PORT: 6379
  218. steps:
  219. - name: Check out repository
  220. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  221. with:
  222. persist-credentials: false
  223. - name: Set up Python
  224. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  225. with:
  226. python-version: '3.12'
  227. cache: pip
  228. - name: Download package artifacts
  229. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  230. with:
  231. name: python-package-distributions
  232. path: dist/
  233. - name: Install system build dependencies for psycopg
  234. run: sudo apt-get update && sudo apt-get install -y libpq-dev
  235. - name: Install wheel into a clean virtual environment
  236. run: |
  237. python -m venv "$RUNNER_TEMP/netbox-wheel-venv"
  238. "$RUNNER_TEMP/netbox-wheel-venv/bin/python" -m pip install --upgrade pip
  239. "$RUNNER_TEMP/netbox-wheel-venv/bin/python" -m pip install dist/*.whl
  240. - name: Run NetBox smoke checks
  241. env:
  242. # STATIC_ROOT is not a configuration parameter; NETBOX_ROOT places it under the scratch base.
  243. NETBOX_ROOT: ${{ runner.temp }}/netbox-smoketest
  244. NETBOX_SMOKETEST_BASE: ${{ runner.temp }}/netbox-smoketest
  245. PYTHONPATH: ${{ github.workspace }}/scripts
  246. run: |
  247. "$RUNNER_TEMP/netbox-wheel-venv/bin/netbox" check
  248. "$RUNNER_TEMP/netbox-wheel-venv/bin/netbox" upgrade --no-input
  249. test -f "$NETBOX_SMOKETEST_BASE/static/docs/index.html" || { echo "bundled documentation was not collected to STATIC_ROOT"; exit 1; }
  250. test -f "$NETBOX_SMOKETEST_BASE/static/docs/models/dcim/device/index.html" || { echo "model documentation page was not collected"; exit 1; }
  251. - name: Smoke-test netbox setup from the wheel
  252. run: |
  253. "$RUNNER_TEMP/netbox-wheel-venv/bin/netbox" setup --target "$RUNNER_TEMP/nbroot"
  254. diff -q "$RUNNER_TEMP/nbroot/conf/configuration.py" netbox/netbox/configuration_example.py
  255. for f in apache.conf gunicorn.py netbox-rq.service netbox.env netbox.service nginx.conf uwsgi.ini; do
  256. diff -q "$RUNNER_TEMP/nbroot/contrib/$f" "contrib/$f"
  257. done
  258. publish-testpypi:
  259. name: Publish package to Test PyPI
  260. runs-on: ubuntu-latest
  261. needs: [smoke-test, cli-smoke-test, verify-dependencies, verify-sdist]
  262. # Publishing always requires a v* tag ref: a tag push publishes to Test PyPI
  263. # automatically, and a manual dispatch does the same when the chosen ref is a v* tag.
  264. # Branch dispatches still run the build, verify, and smoke-test jobs (a useful dry run)
  265. # but the publish job is skipped. Production PyPI publishing is intentionally absent
  266. # during the v4.6.x preview; it arrives with the v4.7.0 feature branch.
  267. # startsWith() only routes to this job (workflow `if:` expressions cannot regex-match);
  268. # the exact tag format (v<release.yaml version>) is enforced below by the "Enforce
  269. # release tag format" step and scripts/verify_release_tag.py before any upload.
  270. if: startsWith(github.ref, 'refs/tags/v') && (github.event_name == 'push' || github.event_name == 'workflow_dispatch')
  271. environment:
  272. name: testpypi
  273. url: https://test.pypi.org/p/netbox
  274. permissions:
  275. contents: read
  276. id-token: write
  277. steps:
  278. - name: Enforce release tag format
  279. env:
  280. TAG: ${{ github.ref_name }}
  281. run: |
  282. [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || {
  283. echo "Ref '$TAG' is not a release tag of the form vX.Y.Z[-designation]"
  284. exit 1
  285. }
  286. - name: Check out repository
  287. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  288. with:
  289. persist-credentials: false
  290. - name: Set up Python
  291. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  292. with:
  293. python-version: '3.12'
  294. - name: Install tooling
  295. run: python -m pip install --upgrade pip packaging
  296. - name: Download package artifacts
  297. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  298. with:
  299. name: python-package-distributions
  300. path: dist/
  301. - name: Verify the git tag matches the built version
  302. run: python scripts/verify_release_tag.py "${{ github.ref_name }}" dist/*.whl
  303. - name: Publish package distributions to Test PyPI
  304. # Bundles twine 7.0.0 and packaging 26.2 (requirements/runtime.txt).
  305. # Keep EXPECTED_TWINE_VERSION and EXPECTED_PACKAGING_VERSION aligned when updating this action.
  306. uses: pypa/gh-action-pypi-publish@dc37677b2e1c63e2034f94d8a5b11f265b73ba33 # v1.14.2
  307. with:
  308. repository-url: https://test.pypi.org/legacy/