release.yml 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
  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. steps:
  31. - name: Check out repository
  32. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  33. with:
  34. persist-credentials: false
  35. - name: Set up Python
  36. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  37. with:
  38. python-version: '3.12'
  39. cache: pip
  40. - name: Install build tooling
  41. run: python -m pip install --upgrade build twine packaging
  42. - name: Install documentation toolchain
  43. run: python -m pip install -r requirements.txt
  44. - name: Render the documentation
  45. # -c = clean cache, -s = strict (abort on warnings); verify_wheel_contents.py
  46. # additionally guards against a partial render reaching the wheel.
  47. run: zensical build -c -s
  48. - name: Build sdist and wheel
  49. run: python -m build
  50. - name: Check package metadata
  51. run: twine check dist/*
  52. - name: Verify the release tag
  53. # Both checks run here, in the unprivileged build job, against the wheel that becomes this
  54. # run's artifact, so neither publish job has to check out the repository or execute its
  55. # scripts while holding id-token: write. A failure here skips every downstream job.
  56. if: startsWith(github.ref, 'refs/tags/v')
  57. env:
  58. TAG: ${{ github.ref_name }}
  59. run: |
  60. [[ "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+([.-][0-9A-Za-z.-]+)?$ ]] || {
  61. echo "Ref '$TAG' is not a release tag of the form vX.Y.Z[-designation]"
  62. exit 1
  63. }
  64. python scripts/verify_release_tag.py "$TAG" dist/*.whl
  65. - name: Upload package artifacts
  66. uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
  67. with:
  68. name: python-package-distributions
  69. path: dist/
  70. if-no-files-found: error
  71. verify-dependencies:
  72. name: Verify dependency pins are in sync
  73. runs-on: ubuntu-latest
  74. needs: build
  75. steps:
  76. - name: Check out repository
  77. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  78. with:
  79. persist-credentials: false
  80. - name: Set up Python
  81. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  82. with:
  83. python-version: '3.12'
  84. cache: pip
  85. - name: Install packaging
  86. run: python -m pip install packaging
  87. - name: Verify requirements.txt is consistent with base_requirements.txt
  88. run: python scripts/verify_dependencies.py
  89. - name: Download package artifacts
  90. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  91. with:
  92. name: python-package-distributions
  93. path: dist/
  94. - name: Verify wheel Requires-Dist matches requirements.txt
  95. run: python scripts/verify_wheel_metadata.py dist/*.whl
  96. - name: Verify wheel excludes live configuration files
  97. run: python scripts/verify_wheel_contents.py dist/*.whl
  98. verify-sdist:
  99. name: Verify the sdist builds a wheel
  100. runs-on: ubuntu-latest
  101. needs: build
  102. steps:
  103. - name: Check out repository
  104. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  105. with:
  106. persist-credentials: false
  107. - name: Set up Python
  108. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  109. with:
  110. python-version: '3.12'
  111. cache: pip
  112. - name: Install tooling
  113. run: python -m pip install --upgrade pip packaging
  114. - name: Download package artifacts
  115. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  116. with:
  117. name: python-package-distributions
  118. path: dist/
  119. - name: Verify the sdist contents
  120. run: |
  121. python scripts/verify_sdist_contents.py dist/*.tar.gz
  122. - name: Build a wheel from the sdist
  123. run: |
  124. python -m pip wheel --no-deps dist/*.tar.gz -w sdist-wheel/
  125. - name: Verify the sdist-built wheel
  126. run: |
  127. python scripts/verify_wheel_metadata.py sdist-wheel/*.whl
  128. python scripts/verify_wheel_contents.py sdist-wheel/*.whl
  129. cli-smoke-test:
  130. name: Smoke test wheel CLI (no dependencies)
  131. runs-on: ubuntu-latest
  132. needs: build
  133. # The pre-configuration CLI paths are stdlib-only, so a --no-deps install suffices.
  134. # Unlike smoke-test, this job also runs on pull requests.
  135. steps:
  136. - name: Set up Python
  137. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  138. with:
  139. python-version: '3.12'
  140. - name: Download package artifacts
  141. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  142. with:
  143. name: python-package-distributions
  144. path: dist/
  145. - name: Install wheel without dependencies
  146. run: |
  147. python -m venv "$RUNNER_TEMP/netbox-cli-venv"
  148. "$RUNNER_TEMP/netbox-cli-venv/bin/python" -m pip install --no-deps dist/*.whl
  149. - name: Exercise the pre-configuration CLI
  150. run: |
  151. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" --version
  152. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" version
  153. "$RUNNER_TEMP/netbox-cli-venv/bin/python" -m netbox --version
  154. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" secret-key | grep -Eq '^.{50}$' || { echo "secret-key not 50 chars"; exit 1; }
  155. - name: Smoke-test netbox setup from the wheel
  156. run: |
  157. "$RUNNER_TEMP/netbox-cli-venv/bin/netbox" setup --target "$RUNNER_TEMP/nbroot"
  158. for f in "$RUNNER_TEMP/nbroot/conf/__init__.py" "$RUNNER_TEMP/nbroot/conf/configuration.py" "$RUNNER_TEMP/nbroot/local_requirements.txt"; do
  159. test -f "$f" || { echo "missing $f"; exit 1; }
  160. done
  161. for f in apache.conf gunicorn.py netbox-rq.service netbox.env netbox.service nginx.conf uwsgi.ini; do
  162. test -s "$RUNNER_TEMP/nbroot/contrib/$f" || { echo "missing or empty contrib/$f"; exit 1; }
  163. done
  164. smoke-test:
  165. name: Smoke test wheel install
  166. runs-on: ubuntu-latest
  167. needs: build
  168. # The wheel install + database migration is expensive; only run it for tag
  169. # pushes and manual dispatch, not on every packaging-related pull request.
  170. # cli-smoke-test provides lightweight, dependency-free CLI coverage on every PR instead.
  171. if: github.event_name != 'pull_request'
  172. services:
  173. postgres:
  174. image: postgres:17
  175. env:
  176. POSTGRES_DB: netbox
  177. POSTGRES_USER: netbox
  178. POSTGRES_PASSWORD: netbox
  179. ports:
  180. - 5432:5432
  181. options: >-
  182. --health-cmd "pg_isready -U netbox -d netbox"
  183. --health-interval 10s
  184. --health-timeout 5s
  185. --health-retries 5
  186. redis:
  187. image: redis:7
  188. ports:
  189. - 6379:6379
  190. options: >-
  191. --health-cmd "redis-cli ping"
  192. --health-interval 10s
  193. --health-timeout 5s
  194. --health-retries 5
  195. env:
  196. NETBOX_CONFIGURATION: smoketest_configuration
  197. POSTGRES_DB: netbox
  198. POSTGRES_USER: netbox
  199. POSTGRES_PASSWORD: netbox
  200. POSTGRES_HOST: 127.0.0.1
  201. POSTGRES_PORT: 5432
  202. REDIS_HOST: 127.0.0.1
  203. REDIS_PORT: 6379
  204. steps:
  205. - name: Check out repository
  206. uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
  207. with:
  208. persist-credentials: false
  209. - name: Set up Python
  210. uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
  211. with:
  212. python-version: '3.12'
  213. cache: pip
  214. - name: Download package artifacts
  215. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  216. with:
  217. name: python-package-distributions
  218. path: dist/
  219. - name: Install system build dependencies for psycopg
  220. run: sudo apt-get update && sudo apt-get install -y libpq-dev
  221. - name: Install wheel into a clean virtual environment
  222. run: |
  223. python -m venv "$RUNNER_TEMP/netbox-wheel-venv"
  224. "$RUNNER_TEMP/netbox-wheel-venv/bin/python" -m pip install --upgrade pip
  225. "$RUNNER_TEMP/netbox-wheel-venv/bin/python" -m pip install dist/*.whl
  226. - name: Run NetBox smoke checks
  227. env:
  228. # STATIC_ROOT is not a configuration parameter; NETBOX_ROOT places it under the scratch base.
  229. NETBOX_ROOT: ${{ runner.temp }}/netbox-smoketest
  230. NETBOX_SMOKETEST_BASE: ${{ runner.temp }}/netbox-smoketest
  231. PYTHONPATH: ${{ github.workspace }}/scripts
  232. run: |
  233. "$RUNNER_TEMP/netbox-wheel-venv/bin/netbox" check
  234. "$RUNNER_TEMP/netbox-wheel-venv/bin/netbox" upgrade --no-input
  235. test -f "$NETBOX_SMOKETEST_BASE/static/docs/index.html" || { echo "bundled documentation was not collected to STATIC_ROOT"; exit 1; }
  236. test -f "$NETBOX_SMOKETEST_BASE/static/docs/models/dcim/device/index.html" || { echo "model documentation page was not collected"; exit 1; }
  237. - name: Smoke-test netbox setup from the wheel
  238. run: |
  239. "$RUNNER_TEMP/netbox-wheel-venv/bin/netbox" setup --target "$RUNNER_TEMP/nbroot"
  240. diff -q "$RUNNER_TEMP/nbroot/conf/configuration.py" netbox/netbox/configuration_example.py
  241. for f in apache.conf gunicorn.py netbox-rq.service netbox.env netbox.service nginx.conf uwsgi.ini; do
  242. diff -q "$RUNNER_TEMP/nbroot/contrib/$f" "contrib/$f"
  243. done
  244. publish-testpypi:
  245. name: Publish package to Test PyPI
  246. runs-on: ubuntu-latest
  247. needs: [smoke-test, cli-smoke-test, verify-dependencies, verify-sdist]
  248. # Test PyPI remains an opt-in rehearsal channel: only a manual dispatch from a v* tag publishes
  249. # here, so the publish path can be exercised against a real index without touching production.
  250. # A branch dispatch still runs the build, verify, and smoke-test jobs as a dry run, with both
  251. # publish jobs skipped.
  252. # startsWith() is only a coarse route to this job; workflow if: expressions cannot regex-match.
  253. # The tag format and the tag-to-wheel version match are enforced in the build job, which fails
  254. # the whole run before anything is uploaded.
  255. if: github.event_name == 'workflow_dispatch' && startsWith(github.ref, 'refs/tags/v')
  256. environment:
  257. name: testpypi
  258. url: https://test.pypi.org/p/netbox
  259. permissions:
  260. contents: read
  261. id-token: write
  262. steps:
  263. - name: Download package artifacts
  264. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  265. with:
  266. name: python-package-distributions
  267. path: dist/
  268. - name: Publish package distributions to Test PyPI
  269. uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
  270. with:
  271. repository-url: https://test.pypi.org/legacy/
  272. print-hash: true
  273. publish-pypi:
  274. name: Publish package to PyPI
  275. runs-on: ubuntu-latest
  276. needs: [smoke-test, cli-smoke-test, verify-dependencies, verify-sdist]
  277. # A v* tag push is the production path. Test PyPI is an opt-in rehearsal rather than a promotion
  278. # stage, so it is deliberately absent from this job's needs: an outage, a duplicate filename, or
  279. # a misconfiguration on a test service must not block a verified production release. The four
  280. # verification jobs above already ran against these exact artifacts. The protected pypi
  281. # environment supplies the deliberate approval step, and because accepted PyPI filenames cannot
  282. # be replaced or reused, a filename the index already holds fails the job instead of being
  283. # skipped.
  284. if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
  285. environment:
  286. name: pypi
  287. url: https://pypi.org/p/netbox
  288. permissions:
  289. contents: read
  290. id-token: write
  291. steps:
  292. - name: Download package artifacts
  293. uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
  294. with:
  295. name: python-package-distributions
  296. path: dist/
  297. - name: Publish package distributions to PyPI
  298. uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
  299. with:
  300. print-hash: true