build-and-release.yml 11 KB

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