build-and-release.yml 9.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275
  1. ---
  2. name: "Build & Release pipeline"
  3. on:
  4. pull_request:
  5. paths:
  6. - '.github/workflows/build-and-release.yml'
  7. - '.goreleaser.yml'
  8. - 'Dockerfile.multiarches'
  9. - 'Dockerfile.singlearch'
  10. - 'Makefile'
  11. - 'frontend/**'
  12. - 'integration-tests/**'
  13. - 'proto/**'
  14. - 'service/**'
  15. - 'var/windows/**'
  16. workflow_dispatch:
  17. push:
  18. tags:
  19. - '*'
  20. branches:
  21. - main
  22. - next
  23. - beta
  24. paths:
  25. - '.github/workflows/build-and-release.yml'
  26. - '.goreleaser.yml'
  27. - 'Dockerfile.multiarches'
  28. - 'Dockerfile.singlearch'
  29. - 'Makefile'
  30. - 'frontend/**'
  31. - 'integration-tests/**'
  32. - 'proto/**'
  33. - 'service/**'
  34. - 'var/windows/**'
  35. jobs:
  36. build:
  37. runs-on: ubuntu-latest
  38. env:
  39. # semantic-release only publishes from main (.releaserc.yaml); goreleaser runs in publishCmd.
  40. WILL_PUBLISH: ${{ github.ref == 'refs/heads/main' && github.ref_type != 'tag' && github.event_name != 'pull_request' }}
  41. outputs:
  42. new_release_published: ${{ steps.release.outputs.new_release_published }}
  43. new_release_git_tag: ${{ steps.release.outputs.new_release_git_tag }}
  44. windows_zip_artifact_id: ${{ steps.upload-windows-zip.outputs.artifact-id }}
  45. windows_msi_artifact_id: ${{ steps.upload-windows-msi.outputs.artifact-id }}
  46. steps:
  47. - name: Checkout
  48. uses: actions/checkout@v6
  49. with:
  50. fetch-depth: ${{ env.WILL_PUBLISH == 'true' && '0' || '1' }}
  51. - name: Set up QEMU
  52. if: env.WILL_PUBLISH == 'true'
  53. id: qemu
  54. uses: docker/setup-qemu-action@v4
  55. with:
  56. image: tonistiigi/binfmt:latest
  57. platforms: arm64,arm
  58. - name: Setup node
  59. uses: actions/setup-node@v6.4.0
  60. with:
  61. node-version: '22'
  62. cache: 'npm'
  63. cache-dependency-path: |
  64. frontend/package-lock.json
  65. integration-tests/package-lock.json
  66. - name: Setup Go
  67. uses: actions/setup-go@v6
  68. with:
  69. go-version-file: 'service/go.mod'
  70. cache: true
  71. cache-dependency-path: 'service/go.mod'
  72. - name: Print go version
  73. run: go version
  74. - name: Login to Docker Hub
  75. if: env.WILL_PUBLISH == 'true'
  76. uses: docker/login-action@v4
  77. with:
  78. username: ${{ secrets.DOCKERHUB_USERNAME }}
  79. password: ${{ secrets.DOCKERHUB_KEY }}
  80. - name: Login to ghcr
  81. if: env.WILL_PUBLISH == 'true'
  82. uses: docker/login-action@v4
  83. with:
  84. registry: ghcr.io
  85. username: ${{ github.actor }}
  86. password: ${{ secrets.CONTAINER_TOKEN }}
  87. - name: get date
  88. run: |
  89. echo "DATE=$(date +'%Y-%m-%d')" >> "$GITHUB_ENV"
  90. - name: build and unit tests
  91. run: |
  92. make -w webui-dist &
  93. webui_pid=$!
  94. make -w service-unittests &
  95. service_tests_pid=$!
  96. webui_status=0
  97. service_tests_status=0
  98. wait "$webui_pid" || webui_status=$?
  99. wait "$service_tests_pid" || service_tests_status=$?
  100. if (( webui_status != 0 || service_tests_status != 0 )); then
  101. exit 1
  102. fi
  103. make -w frontend-unittests
  104. - name: integration tests
  105. env:
  106. SKIP_WEBUI: 1
  107. run: cd integration-tests && make -w
  108. - name: Archive integration tests
  109. uses: actions/upload-artifact@v7
  110. if: failure()
  111. with:
  112. name: "OliveTin-integration-tests-${{ env.DATE }}-${{ github.sha }}"
  113. path: |
  114. integration-tests
  115. !integration-tests/node_modules
  116. - name: Install wixl and msitools
  117. if: env.WILL_PUBLISH == 'true'
  118. run: sudo apt-get update && sudo apt-get install -y wixl msitools
  119. - name: Install goreleaser
  120. if: env.WILL_PUBLISH == 'true'
  121. uses: goreleaser/goreleaser-action@v7
  122. with:
  123. install-only: true
  124. - name: Set up Docker Buildx
  125. if: env.WILL_PUBLISH == 'true'
  126. uses: docker/setup-buildx-action@v4
  127. - name: Verify macOS signing certificate chain
  128. if: env.WILL_PUBLISH == 'true'
  129. env:
  130. MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
  131. MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
  132. run: ./var/macos/verify-macos-sign-p12.sh
  133. - name: release
  134. id: release
  135. if: env.WILL_PUBLISH == 'true'
  136. uses: cycjimmy/semantic-release-action@v5
  137. with:
  138. extra_plugins: |
  139. @semantic-release/commit-analyzer
  140. @semantic-release/exec
  141. @semantic-release/git
  142. env:
  143. GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
  144. GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
  145. MACOS_SIGN_P12: ${{ secrets.MACOS_SIGN_P12 }}
  146. MACOS_SIGN_PASSWORD: ${{ secrets.MACOS_SIGN_PASSWORD }}
  147. MACOS_NOTARY_KEY: ${{ secrets.MACOS_NOTARY_KEY }}
  148. MACOS_NOTARY_KEY_ID: ${{ secrets.MACOS_NOTARY_KEY_ID }}
  149. MACOS_NOTARY_ISSUER_ID: ${{ secrets.MACOS_NOTARY_ISSUER_ID }}
  150. - name: Upload unsigned Windows zip for SignPath
  151. id: upload-windows-zip
  152. if: steps.release.outputs.new_release_published == 'true'
  153. uses: actions/upload-artifact@v7
  154. with:
  155. # Upload as-is so SignPath receives OliveTin-windows-amd64.zip, not a wrapper zip.
  156. # With archive: false, the artifact name is the filename (`name` is ignored).
  157. path: dist/OliveTin-windows-amd64.zip
  158. archive: false
  159. if-no-files-found: error
  160. - name: Upload unsigned Windows MSI for SignPath
  161. id: upload-windows-msi
  162. if: steps.release.outputs.new_release_published == 'true'
  163. uses: actions/upload-artifact@v7
  164. with:
  165. path: dist/OliveTin-windows-amd64.msi
  166. archive: false
  167. if-no-files-found: error
  168. - name: Archive binaries
  169. if: env.WILL_PUBLISH == 'true'
  170. uses: actions/upload-artifact@v7
  171. with:
  172. name: "OliveTin-snapshot-${{ env.DATE }}-${{ github.sha }}"
  173. path: dist/OliveTin*.*
  174. sign-windows:
  175. name: Sign Windows artifacts (SignPath)
  176. needs: build
  177. if: needs.build.outputs.new_release_published == 'true'
  178. runs-on: ubuntu-latest
  179. permissions:
  180. actions: read
  181. contents: write
  182. steps:
  183. - name: Checkout
  184. uses: actions/checkout@v6
  185. with:
  186. persist-credentials: false
  187. - name: Require SignPath configuration
  188. env:
  189. SIGNPATH_API_TOKEN: ${{ secrets.SIGNPATH_API_TOKEN }}
  190. SIGNPATH_ORGANIZATION_ID: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
  191. SIGNPATH_PROJECT_SLUG: ${{ vars.SIGNPATH_PROJECT_SLUG }}
  192. SIGNPATH_SIGNING_POLICY_SLUG: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
  193. run: |
  194. missing=0
  195. for name in SIGNPATH_API_TOKEN SIGNPATH_ORGANIZATION_ID SIGNPATH_PROJECT_SLUG SIGNPATH_SIGNING_POLICY_SLUG; do
  196. if [[ -z "${!name}" ]]; then
  197. echo "Missing required SignPath setting: ${name}" >&2
  198. missing=1
  199. fi
  200. done
  201. if [[ "${missing}" -ne 0 ]]; then
  202. echo "SignPath secrets/vars are required to upload signed Windows assets. Configure them (see docs/modules/dev/pages/signing.adoc)." >&2
  203. exit 1
  204. fi
  205. - name: Sign Windows zip
  206. uses: signpath/github-action-submit-signing-request@v2
  207. with:
  208. api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
  209. organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
  210. project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
  211. signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
  212. artifact-configuration-slug: windows-zip
  213. github-artifact-id: ${{ needs.build.outputs.windows_zip_artifact_id }}
  214. wait-for-completion: true
  215. # Preserve the signed .zip/.msi files; default decompress would unpack the zip.
  216. skip-decompress: true
  217. output-artifact-directory: signed-windows-zip
  218. - name: Sign Windows MSI
  219. uses: signpath/github-action-submit-signing-request@v2
  220. with:
  221. api-token: ${{ secrets.SIGNPATH_API_TOKEN }}
  222. organization-id: ${{ vars.SIGNPATH_ORGANIZATION_ID }}
  223. project-slug: ${{ vars.SIGNPATH_PROJECT_SLUG }}
  224. signing-policy-slug: ${{ vars.SIGNPATH_SIGNING_POLICY_SLUG }}
  225. artifact-configuration-slug: windows-msi
  226. github-artifact-id: ${{ needs.build.outputs.windows_msi_artifact_id }}
  227. wait-for-completion: true
  228. skip-decompress: true
  229. output-artifact-directory: signed-windows-msi
  230. - name: Upload signed Windows assets
  231. env:
  232. GH_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
  233. GITHUB_TOKEN: ${{ secrets.CONTAINER_TOKEN }}
  234. run: |
  235. # SignPath may name the signed zip *.zip.zip (one compression level, bad Content-Disposition).
  236. if [[ -f signed-windows-zip/OliveTin-windows-amd64.zip.zip ]]; then
  237. mv signed-windows-zip/OliveTin-windows-amd64.zip.zip \
  238. signed-windows-zip/OliveTin-windows-amd64.zip
  239. fi
  240. zip_path="$(find signed-windows-zip -type f -name 'OliveTin-windows-amd64.zip' | head -n 1)"
  241. msi_path="$(find signed-windows-msi -type f -name 'OliveTin-windows-amd64.msi' | head -n 1)"
  242. if [[ -z "${zip_path}" || -z "${msi_path}" ]]; then
  243. echo "Signed Windows artifacts not found after SignPath:" >&2
  244. find signed-windows-zip signed-windows-msi -type f >&2 || true
  245. exit 1
  246. fi
  247. ./var/windows/signpath-publish-signed.sh \
  248. "${{ needs.build.outputs.new_release_git_tag }}" \
  249. "${zip_path}" \
  250. "${msi_path}"