.github/workflows/release.yml 14.0 K raw
1
# This file was autogenerated by dist: https://opensource.axo.dev/cargo-dist/
2
#
3
# Copyright 2022-2024, axodotdev
4
# SPDX-License-Identifier: MIT or Apache-2.0
5
#
6
# CI that:
7
#
8
# * checks for a Git Tag that looks like a release
9
# * builds artifacts with dist (archives, installers, hashes)
10
# * uploads those artifacts to temporary workflow zip
11
# * on success, uploads the artifacts to a GitHub Release
12
#
13
# Note that the GitHub Release will be created with a generated
14
# title/body based on your changelogs.
15
16
name: Release
17
permissions:
18
  "contents": "write"
19
20
# This task will run whenever you push a git tag that looks like a version
21
# like "1.0.0", "v0.1.0-prerelease.1", "my-app/0.1.0", "releases/v1.0.0", etc.
22
# Various formats will be parsed into a VERSION and an optional PACKAGE_NAME, where
23
# PACKAGE_NAME must be the name of a Cargo package in your workspace, and VERSION
24
# must be a Cargo-style SemVer Version (must have at least major.minor.patch).
25
#
26
# If PACKAGE_NAME is specified, then the announcement will be for that
27
# package (erroring out if it doesn't have the given version or isn't dist-able).
28
#
29
# If PACKAGE_NAME isn't specified, then the announcement will be for all
30
# (dist-able) packages in the workspace with that version (this mode is
31
# intended for workspaces with only one dist-able package, or with all dist-able
32
# packages versioned/released in lockstep).
33
#
34
# If you push multiple tags at once, separate instances of this workflow will
35
# spin up, creating an independent announcement for each one. However, GitHub
36
# will hard limit this to 3 tags per commit, as it will assume more tags is a
37
# mistake.
38
#
39
# If there's a prerelease-style suffix to the version, then the release(s)
40
# will be marked as a prerelease.
41
on:
42
  pull_request:
43
  push:
44
    tags:
45
      - '**[0-9]+.[0-9]+.[0-9]+*'
46
47
jobs:
48
  # Run 'dist plan' (or host) to determine what tasks we need to do
49
  plan:
50
    runs-on: "ubuntu-24.04"
51
    outputs:
52
      val: ${{ steps.plan.outputs.manifest }}
53
      tag: ${{ !github.event.pull_request && github.ref_name || '' }}
54
      tag-flag: ${{ !github.event.pull_request && format('--tag={0}', github.ref_name) || '' }}
55
      publishing: ${{ !github.event.pull_request }}
56
    env:
57
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
58
    steps:
59
      - uses: actions/checkout@v4
60
        with:
61
          submodules: recursive
62
      - name: Install dist
63
        # we specify bash to get pipefail; it guards against the `curl` command
64
        # failing. otherwise `sh` won't catch that `curl` returned non-0
65
        shell: bash
66
        run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.28.0/cargo-dist-installer.sh | sh"
67
      - name: Cache dist
68
        uses: actions/upload-artifact@v4
69
        with:
70
          name: cargo-dist-cache
71
          path: ~/.cargo/bin/dist
72
      # sure would be cool if github gave us proper conditionals...
73
      # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible
74
      # functionality based on whether this is a pull_request, and whether it's from a fork.
75
      # (PRs run on the *source* but secrets are usually on the *target* -- that's *good*
76
      # but also really annoying to build CI around when it needs secrets to work right.)
77
      - id: plan
78
        run: |
79
          dist ${{ (!github.event.pull_request && format('host --steps=create --tag={0}', github.ref_name)) || 'plan' }} --output-format=json --allow-dirty > plan-dist-manifest.json
80
          echo "dist ran successfully"
81
          cat plan-dist-manifest.json
82
          echo "manifest=$(jq -c "." plan-dist-manifest.json)" >> "$GITHUB_OUTPUT"
83
      - name: "Upload dist-manifest.json"
84
        uses: actions/upload-artifact@v4
85
        with:
86
          name: artifacts-plan-dist-manifest
87
          path: plan-dist-manifest.json
88
89
  # Build and packages all the platform-specific things
90
  build-local-artifacts:
91
    name: build-local-artifacts (${{ join(matrix.targets, ', ') }})
92
    # Let the initial task tell us to not run (currently very blunt)
93
    needs:
94
      - plan
95
    if: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix.include != null && (needs.plan.outputs.publishing == 'true' || fromJson(needs.plan.outputs.val).ci.github.pr_run_mode == 'upload') }}
96
    strategy:
97
      fail-fast: false
98
      # Target platforms/runners are computed by dist in create-release.
99
      # Each member of the matrix has the following arguments:
100
      #
101
      # - runner: the github runner
102
      # - dist-args: cli flags to pass to dist
103
      # - install-dist: expression to run to install dist on the runner
104
      #
105
      # Typically there will be:
106
      # - 1 "global" task that builds universal installers
107
      # - N "local" tasks that build each platform's binaries and platform-specific installers
108
      matrix: ${{ fromJson(needs.plan.outputs.val).ci.github.artifacts_matrix }}
109
    runs-on: ${{ matrix.runner }}
110
    container: ${{ matrix.container && matrix.container.image || null }}
111
    env:
112
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
113
      BUILD_MANIFEST_NAME: target/distrib/${{ join(matrix.targets, '-') }}-dist-manifest.json
114
    steps:
115
      - name: enable windows longpaths
116
        run: |
117
          git config --global core.longpaths true
118
      - uses: actions/checkout@v4
119
        with:
120
          submodules: recursive
121
      - name: Install Rust non-interactively if not already installed
122
        if: ${{ matrix.container }}
123
        run: |
124
          if ! command -v cargo > /dev/null 2>&1; then
125
            curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
126
            echo "$HOME/.cargo/bin" >> $GITHUB_PATH
127
          fi
128
      - name: Install dist
129
        run: ${{ matrix.install_dist.run }}
130
      # Get the dist-manifest
131
      - name: Fetch local artifacts
132
        uses: actions/download-artifact@v4
133
        with:
134
          pattern: artifacts-*
135
          path: target/distrib/
136
          merge-multiple: true
137
      - name: Install dependencies
138
        run: |
139
          ${{ matrix.packages_install }}
140
      - name: Build artifacts
141
        run: |
142
          # Actually do builds and make zips and whatnot
143
          dist build ${{ needs.plan.outputs.tag-flag }} --print=linkage --output-format=json --allow-dirty ${{ matrix.dist_args }} > dist-manifest.json
144
          echo "dist ran successfully"
145
      - id: cargo-dist
146
        name: Post-build
147
        # We force bash here just because github makes it really hard to get values up
148
        # to "real" actions without writing to env-vars, and writing to env-vars has
149
        # inconsistent syntax between shell and powershell.
150
        shell: bash
151
        run: |
152
          # Parse out what we just built and upload it to scratch storage
153
          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
154
          dist print-upload-files-from-manifest --manifest dist-manifest.json >> "$GITHUB_OUTPUT"
155
          echo "EOF" >> "$GITHUB_OUTPUT"
156
157
          cp dist-manifest.json "$BUILD_MANIFEST_NAME"
158
      - name: "Upload artifacts"
159
        uses: actions/upload-artifact@v4
160
        with:
161
          name: artifacts-build-local-${{ join(matrix.targets, '_') }}
162
          path: |
163
            ${{ steps.cargo-dist.outputs.paths }}
164
            ${{ env.BUILD_MANIFEST_NAME }}
165
166
  # Build and package all the platform-agnostic(ish) things
167
  build-global-artifacts:
168
    needs:
169
      - plan
170
      - build-local-artifacts
171
    runs-on: "ubuntu-24.04"
172
    env:
173
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
174
      BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json
175
    steps:
176
      - uses: actions/checkout@v4
177
        with:
178
          submodules: recursive
179
      - name: Install cached dist
180
        uses: actions/download-artifact@v4
181
        with:
182
          name: cargo-dist-cache
183
          path: ~/.cargo/bin/
184
      - run: chmod +x ~/.cargo/bin/dist
185
      # Get all the local artifacts for the global tasks to use (for e.g. checksums)
186
      - name: Fetch local artifacts
187
        uses: actions/download-artifact@v4
188
        with:
189
          pattern: artifacts-*
190
          path: target/distrib/
191
          merge-multiple: true
192
      - id: cargo-dist
193
        shell: bash
194
        run: |
195
          dist build ${{ needs.plan.outputs.tag-flag }} --output-format=json --allow-dirty "--artifacts=global" > dist-manifest.json
196
          echo "dist ran successfully"
197
198
          # Parse out what we just built and upload it to scratch storage
199
          echo "paths<<EOF" >> "$GITHUB_OUTPUT"
200
          jq --raw-output ".upload_files[]" dist-manifest.json >> "$GITHUB_OUTPUT"
201
          echo "EOF" >> "$GITHUB_OUTPUT"
202
203
          cp dist-manifest.json "$BUILD_MANIFEST_NAME"
204
      - name: "Upload artifacts"
205
        uses: actions/upload-artifact@v4
206
        with:
207
          name: artifacts-build-global
208
          path: |
209
            ${{ steps.cargo-dist.outputs.paths }}
210
            ${{ env.BUILD_MANIFEST_NAME }}
211
  # Determines if we should publish/announce
212
  host:
213
    needs:
214
      - plan
215
      - build-local-artifacts
216
      - build-global-artifacts
217
    # Only run if we're "publishing", and only if local and global didn't fail (skipped is fine)
218
    if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') && (needs.build-local-artifacts.result == 'skipped' || needs.build-local-artifacts.result == 'success') }}
219
    env:
220
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
221
    runs-on: "ubuntu-24.04"
222
    outputs:
223
      val: ${{ steps.host.outputs.manifest }}
224
    steps:
225
      - uses: actions/checkout@v4
226
        with:
227
          submodules: recursive
228
      - name: Install cached dist
229
        uses: actions/download-artifact@v4
230
        with:
231
          name: cargo-dist-cache
232
          path: ~/.cargo/bin/
233
      - run: chmod +x ~/.cargo/bin/dist
234
      # Fetch artifacts from scratch-storage
235
      - name: Fetch artifacts
236
        uses: actions/download-artifact@v4
237
        with:
238
          pattern: artifacts-*
239
          path: target/distrib/
240
          merge-multiple: true
241
      - id: host
242
        shell: bash
243
        run: |
244
          dist host ${{ needs.plan.outputs.tag-flag }} --steps=upload --steps=release --output-format=json > dist-manifest.json
245
          echo "artifacts uploaded and released successfully"
246
          cat dist-manifest.json
247
          echo "manifest=$(jq -c "." dist-manifest.json)" >> "$GITHUB_OUTPUT"
248
      - name: "Upload dist-manifest.json"
249
        uses: actions/upload-artifact@v4
250
        with:
251
          # Overwrite the previous copy
252
          name: artifacts-dist-manifest
253
          path: dist-manifest.json
254
      # Create a GitHub Release while uploading all files to it
255
      - name: "Download GitHub Artifacts"
256
        uses: actions/download-artifact@v4
257
        with:
258
          pattern: artifacts-*
259
          path: artifacts
260
          merge-multiple: true
261
      - name: Cleanup
262
        run: |
263
          # Remove the granular manifests
264
          rm -f artifacts/*-dist-manifest.json
265
      - name: Create GitHub Release
266
        env:
267
          PRERELEASE_FLAG: "${{ fromJson(steps.host.outputs.manifest).announcement_is_prerelease && '--prerelease' || '' }}"
268
          ANNOUNCEMENT_TITLE: "${{ fromJson(steps.host.outputs.manifest).announcement_title }}"
269
          ANNOUNCEMENT_BODY: "${{ fromJson(steps.host.outputs.manifest).announcement_github_body }}"
270
          RELEASE_COMMIT: "${{ github.sha }}"
271
        run: |
272
          # Write and read notes from a file to avoid quoting breaking things
273
          echo "$ANNOUNCEMENT_BODY" > $RUNNER_TEMP/notes.txt
274
275
          gh release create "${{ needs.plan.outputs.tag }}" --target "$RELEASE_COMMIT" $PRERELEASE_FLAG --title "$ANNOUNCEMENT_TITLE" --notes-file "$RUNNER_TEMP/notes.txt" artifacts/*
276
277
  publish-homebrew-formula:
278
    needs:
279
      - plan
280
      - host
281
    runs-on: "ubuntu-24.04"
282
    env:
283
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
284
      PLAN: ${{ needs.plan.outputs.val }}
285
      GITHUB_USER: "axo bot"
286
      GITHUB_EMAIL: "admin+bot@axo.dev"
287
    if: ${{ !fromJson(needs.plan.outputs.val).announcement_is_prerelease || fromJson(needs.plan.outputs.val).publish_prereleases }}
288
    steps:
289
      - uses: actions/checkout@v4
290
        with:
291
          repository: "stevedylandev/homebrew-walletfetch"
292
          token: ${{ secrets.HOMEBREW_TAP_TOKEN }}
293
      # So we have access to the formula
294
      - name: Fetch homebrew formulae
295
        uses: actions/download-artifact@v4
296
        with:
297
          pattern: artifacts-*
298
          path: Formula/
299
          merge-multiple: true
300
      # This is extra complex because you can make your Formula name not match your app name
301
      # so we need to find releases with a *.rb file, and publish with that filename.
302
      - name: Commit formula files
303
        run: |
304
          git config --global user.name "${GITHUB_USER}"
305
          git config --global user.email "${GITHUB_EMAIL}"
306
307
          for release in $(echo "$PLAN" | jq --compact-output '.releases[] | select([.artifacts[] | endswith(".rb")] | any)'); do
308
            filename=$(echo "$release" | jq '.artifacts[] | select(endswith(".rb"))' --raw-output)
309
            name=$(echo "$filename" | sed "s/\.rb$//")
310
            version=$(echo "$release" | jq .app_version --raw-output)
311
312
            export PATH="/home/linuxbrew/.linuxbrew/bin:$PATH"
313
            brew update
314
            # We avoid reformatting user-provided data such as the app description and homepage.
315
            brew style --except-cops FormulaAudit/Homepage,FormulaAudit/Desc,FormulaAuditStrict --fix "Formula/${filename}" || true
316
317
            git add "Formula/${filename}"
318
            git commit -m "${name} ${version}"
319
          done
320
          git push
321
322
  announce:
323
    needs:
324
      - plan
325
      - host
326
      - publish-homebrew-formula
327
    # use "always() && ..." to allow us to wait for all publish jobs while
328
    # still allowing individual publish jobs to skip themselves (for prereleases).
329
    # "host" however must run to completion, no skipping allowed!
330
    if: ${{ always() && needs.host.result == 'success' && (needs.publish-homebrew-formula.result == 'skipped' || needs.publish-homebrew-formula.result == 'success') }}
331
    runs-on: "ubuntu-24.04"
332
    env:
333
      GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
334
    steps:
335
      - uses: actions/checkout@v4
336
        with:
337
          submodules: recursive