返回 CodeWhale
release-republish.yml
根目录 / .github / workflows / release-republish.yml
1 name: Release Republish Channels
2
3 # Recovery for derived distribution channels — the container image and the
4 # Homebrew tap — when a released tag has them missing or stale.
5 #
6 # This is a separate workflow, dispatched from the DEFAULT BRANCH, on purpose.
7 # `workflow_dispatch` reads its input schema from the workflow file at the ref
8 # being dispatched, and `release.yml` requires being dispatched from the tag
9 # itself. Any recovery option added to `release.yml` is therefore unusable on
10 # tags that predate it — which is every tag that could ever need recovery.
11 # Taking the tag as an input sidesteps that entirely.
12 #
13 # It never writes GitHub Release bytes. `release.yml` owns those, and they stay
14 # immutable; this only (re)publishes channels derived from an existing release.
15
16 on:
17 workflow_dispatch:
18 inputs:
19 version:
20 description: 'Released version to republish, without v (e.g. 0.9.1). The tag must already exist and have a published GitHub Release.'
21 required: true
22 type: string
23 channels:
24 description: 'Which derived channels to republish.'
25 required: true
26 default: 'docker+homebrew'
27 type: choice
28 options:
29 - docker+homebrew
30 - docker
31 - homebrew
32
33 concurrency:
34 group: release-republish-${{ inputs.version }}
35 cancel-in-progress: false
36
37 permissions:
38 contents: read
39
40 jobs:
41 resolve:
42 timeout-minutes: 10
43 runs-on: ubuntu-latest
44 outputs:
45 tag: ${{ steps.release.outputs.tag }}
46 sha: ${{ steps.release.outputs.sha }}
47 version: ${{ steps.release.outputs.version }}
48 steps:
49 - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
50 with:
51 fetch-depth: 0
52 - name: Resolve released tag
53 id: release
54 shell: bash
55 env:
56 INPUT_VERSION: ${{ inputs.version }}
57 run: |
58 set -euo pipefail
59
60 if ! [[ "${INPUT_VERSION}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
61 echo "::error::Version '${INPUT_VERSION}' must use X.Y.Z." >&2
62 exit 1
63 fi
64 tag="v${INPUT_VERSION}"
65
66 if ! git rev-parse --verify "refs/tags/${tag}^{commit}" >/dev/null 2>&1; then
67 echo "::error::Tag ${tag} does not exist." >&2
68 exit 1
69 fi
70 sha="$(git rev-parse "refs/tags/${tag}^{commit}")"
71
72 {
73 echo "tag=${tag}"
74 echo "sha=${sha}"
75 echo "version=${INPUT_VERSION}"
76 } >> "${GITHUB_OUTPUT}"
77 echo "Resolved ${tag} -> ${sha}"
78 - name: Require a complete published release
79 # The inverse of release.yml's immutability guard. That one refuses to
80 # run when assets exist; this one refuses to run unless the published
81 # release carries exactly the assets its own checksum manifest lists,
82 # each fully uploaded, because a channel derived from a partial set
83 # would advertise bytes that are not there. The tag may predate the
84 # current inventory, so its manifest, not today's list, is the check.
85 env:
86 GH_TOKEN: ${{ github.token }}
87 TAG: ${{ steps.release.outputs.tag }}
88 run: node scripts/release/verify-release-inventory.js --manifest "${GITHUB_REPOSITORY}" "${TAG}"
89 - name: Verify the remote tag still points at this commit
90 env:
91 EXPECTED_SHA: ${{ steps.release.outputs.sha }}
92 TAG: ${{ steps.release.outputs.tag }}
93 run: |
94 ./scripts/release/verify-remote-tag.sh \
95 "https://github.com/${GITHUB_REPOSITORY}.git" \
96 "${TAG}" \
97 "${EXPECTED_SHA}"
98
99 docker:
100 timeout-minutes: 30
101 needs: resolve
102 if: ${{ contains(inputs.channels, 'docker') }}
103 runs-on: ubuntu-latest
104 permissions:
105 contents: read
106 packages: write
107 steps:
108 - name: Checkout release source
109 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
110 with:
111 ref: ${{ needs.resolve.outputs.sha }}
112 path: source
113 - name: Checkout release infrastructure
114 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
115 with:
116 ref: ${{ needs.resolve.outputs.sha }}
117 path: infra
118 - name: Set up QEMU
119 uses: docker/setup-qemu-action@99012661954931238ded8c8b007157a8430204e1 # v4
120 - name: Set up Docker Buildx
121 uses: docker/setup-buildx-action@594f3bf4285d9ea8dc53c9a0c9c4092420091003 # v4
122 - name: Log in to GitHub Container Registry
123 uses: docker/login-action@dbcb813823bdd20940b903addbd779551569679f # v4
124 with:
125 registry: ghcr.io
126 username: ${{ github.repository_owner }}
127 password: ${{ secrets.GITHUB_TOKEN }}
128 - name: Normalize image name
129 id: image
130 shell: bash
131 run: echo "name=ghcr.io/${GITHUB_REPOSITORY,,}" >> "$GITHUB_OUTPUT"
132 - name: Revalidate release tag before container publish
133 env:
134 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
135 TAG: ${{ needs.resolve.outputs.tag }}
136 run: |
137 ./infra/scripts/release/verify-remote-tag.sh \
138 "https://github.com/${GITHUB_REPOSITORY}.git" \
139 "${TAG}" \
140 "${EXPECTED_SHA}"
141 - name: Build and push
142 uses: docker/build-push-action@c3c9e263c25d99ce0380d002d59b67737d91b0dc # v7
143 env:
144 DOCKER_BUILD_RECORD_UPLOAD: false
145 DOCKER_BUILD_SUMMARY: false
146 with:
147 context: source
148 file: infra/Dockerfile
149 platforms: linux/amd64,linux/arm64
150 push: true
151 build-args: |
152 CODEWHALE_BUILD_SHA=${{ needs.resolve.outputs.sha }}
153 tags: |
154 ${{ steps.image.outputs.name }}:${{ needs.resolve.outputs.tag }}
155 ${{ steps.image.outputs.name }}:${{ needs.resolve.outputs.version }}
156 ${{ steps.image.outputs.name }}:latest
157 cache-from: type=gha
158 cache-to: type=gha,mode=max
159 - name: Smoke published container entrypoints
160 shell: bash
161 env:
162 IMAGE: ${{ steps.image.outputs.name }}:${{ needs.resolve.outputs.tag }}
163 run: |
164 set -euo pipefail
165 docker pull "${IMAGE}"
166 docker run --rm --entrypoint codewhale "${IMAGE}" --version
167 docker run --rm --entrypoint codew "${IMAGE}" --version
168
169 homebrew:
170 timeout-minutes: 20
171 needs: resolve
172 if: ${{ contains(inputs.channels, 'homebrew') }}
173 runs-on: ubuntu-latest
174 permissions:
175 contents: read
176 steps:
177 - name: Check Homebrew tap token
178 id: homebrew-token
179 env:
180 TOKEN: ${{ secrets.HOMEBREW_TAP_PAT || secrets.RELEASE_TAG_PAT }}
181 run: |
182 if [[ -z "${TOKEN:-}" ]]; then
183 echo "::error::No Homebrew tap token configured; cannot republish the tap." >&2
184 exit 1
185 fi
186 # Recovery logic must come from the current protected default branch.
187 # The released bytes remain pinned by the tag and checksum manifest;
188 # checking out the old tag here would also restore the bug being repaired.
189 - name: Checkout release infrastructure
190 uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7
191 with:
192 ref: ${{ github.event.repository.default_branch }}
193 - name: Download checksum manifest
194 env:
195 GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
196 run: |
197 gh release download "${{ needs.resolve.outputs.tag }}" \
198 --repo "${{ github.repository }}" \
199 --pattern 'codewhale-artifacts-sha256.txt' \
200 --dir /tmp
201 - name: Revalidate release tag before Homebrew tap write
202 env:
203 EXPECTED_SHA: ${{ needs.resolve.outputs.sha }}
204 TAG: ${{ needs.resolve.outputs.tag }}
205 run: |
206 ./scripts/release/verify-remote-tag.sh \
207 "https://github.com/${GITHUB_REPOSITORY}.git" \
208 "${TAG}" \
209 "${EXPECTED_SHA}"
210 - name: Update Homebrew tap
211 env:
212 TAG: ${{ needs.resolve.outputs.tag }}
213 MANIFEST: /tmp/codewhale-artifacts-sha256.txt
214 TAP_REPO: Hmbown/homebrew-deepseek-tui
215 TOKEN: ${{ secrets.HOMEBREW_TAP_PAT || secrets.RELEASE_TAG_PAT }}
216 run: bash .github/scripts/update-homebrew-tap.sh
217
217 lines YAML