8585 artifact_name="$PACKAGE_ID-$version-linux-x64.zip"
8686 test -f "release-output/$artifact_name"
8787 test -f release-output/release.json
88+ test -f release-output/catalog-entry.json
8889 test "$(jq -r .packageId release-output/release.json)" = "$PACKAGE_ID"
8990 test "$(jq -r .version release-output/release.json)" = "$version"
9091 test "$(jq -r .artifact.fileName release-output/release.json)" = "$artifact_name"
92+ test "$(jq -r .packageId release-output/catalog-entry.json)" = "$PACKAGE_ID"
93+ test "$(jq -r '.latest | length' release-output/catalog-entry.json)" = 1
94+ test "$(jq -r '.latest[0].version' release-output/catalog-entry.json)" = "$version"
9195
9296 mkdir extracted
9397 unzip -q "release-output/$artifact_name" -d extracted
@@ -158,18 +162,18 @@ jobs:
158162 source="$staging/release-output"
159163 registry='/srv/heartbeat/collector-registry/v1'
160164 versions="$registry/packages/$package_id/versions"
161- target="$versions/$version"
162- pending="$versions/.$version.staging-${{ github.run_id }}-${{ github.run_attempt }}"
165+ version_root="$versions/$version"
166+ target="$version_root/linux-x64"
167+ pending="$version_root/.linux-x64.staging-${{ github.run_id }}-${{ github.run_attempt }}"
163168
164169 test -f "$source/$artifact"
165170 test -f "$source/release.json"
166- install -d -m 0755 -- "$versions "
171+ install -d -m 0755 -- "$version_root "
167172
168173 if [ -d "$target" ]; then
169174 if cmp -s "$source/$artifact" "$target/$artifact" && \
170175 cmp -s "$source/release.json" "$target/release.json"; then
171176 echo "Exact release already exists; treating rerun as idempotent."
172- rm -rf -- "$staging"
173177 exit 0
174178 fi
175179 echo "Refusing to overwrite $package_id $version with different bytes." >&2
@@ -181,15 +185,14 @@ jobs:
181185 install -m 0644 "$source/$artifact" "$pending/$artifact"
182186 install -m 0644 "$source/release.json" "$pending/release.json"
183187 mv -- "$pending" "$target"
184- rm -rf -- "$staging"
185188
186189 - name : Verify the public exact release bytes
187190 shell : bash
188191 run : |
189192 set -Eeuo pipefail
190193 version='${{ needs.build.outputs.version }}'
191194 artifact='${{ needs.build.outputs.artifact_name }}'
192- url="$REGISTRY_BASE_URL/packages/$PACKAGE_ID/versions/$version"
195+ url="$REGISTRY_BASE_URL/packages/$PACKAGE_ID/versions/$version/linux-x64 "
193196 curl --fail --silent --show-error --location \
194197 --retry 12 --retry-delay 5 \
195198 "$url/$artifact?release-run=$GITHUB_RUN_ID" \
@@ -200,3 +203,134 @@ jobs:
200203 --output "$RUNNER_TEMP/release.json"
201204 cmp release-output/$artifact "$RUNNER_TEMP/$artifact"
202205 cmp release-output/release.json "$RUNNER_TEMP/release.json"
206+
207+ - name : Advance the generic Catalog after the exact release is public
208+ uses : appleboy/ssh-action@v1
209+ with :
210+ host : ${{ secrets.SERVER_HOST }}
211+ username : ${{ secrets.SERVER_USER }}
212+ key : ${{ secrets.SERVER_SSH_KEY }}
213+ script : |
214+ set -eu
215+ package_id='${{ env.PACKAGE_ID }}'
216+ version='${{ needs.build.outputs.version }}'
217+ staging='/tmp/heartbeat-collector-release-${{ github.run_id }}-${{ github.run_attempt }}'
218+ entry="$staging/release-output/catalog-entry.json"
219+ registry='/srv/heartbeat/collector-registry/v1'
220+ catalog="$registry/catalog.json"
221+ lock="$registry/.catalog.lock"
222+
223+ command -v jq >/dev/null 2>&1
224+ command -v flock >/dev/null 2>&1
225+ test -f "$entry"
226+ test "$(jq -r .packageId "$entry")" = "$package_id"
227+ test "$(jq -r '.latest | length' "$entry")" = 1
228+ test "$(jq -r '.latest[0].version' "$entry")" = "$version"
229+ target_os=$(jq -r '.latest[0].target.os' "$entry")
230+ target_arch=$(jq -r '.latest[0].target.arch' "$entry")
231+ install -d -m 0755 -- "$registry"
232+
233+ (
234+ flock -x 9
235+ current=''
236+ existing=''
237+ if [ -f "$catalog" ]; then
238+ jq -e '
239+ .schemaVersion == 1 and
240+ (.packages | type == "array") and
241+ ([.packages[].packageId] | length == (unique | length)) and
242+ all(.packages[];
243+ (.latest | type == "array") and
244+ ([.latest[] | "\(.target.os)/\(.target.arch)"] | length == (unique | length)))
245+ ' "$catalog" >/dev/null
246+ existing=$(jq -c --arg id "$package_id" \
247+ '.packages[] | select(.packageId == $id)' "$catalog")
248+ current=$(jq -r --arg id "$package_id" --arg os "$target_os" --arg arch "$target_arch" \
249+ '.packages[] | select(.packageId == $id) | .latest[] |
250+ select(.target.os == $os and .target.arch == $arch) | .version' "$catalog")
251+ fi
252+
253+ if [ -n "$current" ] && [ "$current" != "$version" ] && \
254+ [ "$(printf '%s\n%s\n' "$current" "$version" | sort -V | tail -n 1)" = "$current" ]; then
255+ echo "Catalog already points at newer $package_id $current; leaving it unchanged."
256+ exit 0
257+ fi
258+
259+ if [ "$current" = "$version" ]; then
260+ if jq -e --argjson expected "$(cat "$entry")" --arg id "$package_id" \
261+ --arg os "$target_os" --arg arch "$target_arch" '
262+ .packages[] | select(.packageId == $id) as $actual |
263+ $actual.displayName == $expected.displayName and
264+ $actual.summary == $expected.summary and
265+ ($actual.latest[] | select(.target.os == $os and .target.arch == $arch)) == $expected.latest[0]
266+ ' "$catalog" >/dev/null; then
267+ echo "Catalog entry already exists; treating rerun as idempotent."
268+ exit 0
269+ fi
270+ echo "Refusing to replace $package_id $target_os/$target_arch $version Catalog metadata with different bytes." >&2
271+ exit 1
272+ fi
273+
274+ incoming=$(cat "$entry")
275+ if [ -n "$existing" ]; then
276+ if ! jq -e --argjson existing "$existing" --argjson incoming "$incoming" \
277+ '$existing.displayName == $incoming.displayName and
278+ $existing.summary == $incoming.summary' >/dev/null; then
279+ echo "Refusing to change shared Catalog presentation while other targets may exist." >&2
280+ exit 1
281+ fi
282+ merged=$(jq -n --argjson existing "$existing" --argjson incoming "$incoming" \
283+ --arg os "$target_os" --arg arch "$target_arch" '
284+ $existing | .latest = (
285+ [.latest[] | select(.target.os != $os or .target.arch != $arch)] +
286+ $incoming.latest |
287+ sort_by(.target.os, .target.arch)
288+ )
289+ ')
290+ else
291+ merged=$incoming
292+ fi
293+
294+ temporary="$registry/.catalog.json.${{ github.run_id }}-${{ github.run_attempt }}"
295+ if [ -f "$catalog" ]; then
296+ jq --arg id "$package_id" --argjson merged "$merged" \
297+ '.packages = ([.packages[] | select(.packageId != $id)] + [$merged] | sort_by(.packageId))' \
298+ "$catalog" > "$temporary"
299+ else
300+ jq -n --argjson entry "$merged" \
301+ '{schemaVersion: 1, packages: [$entry]}' > "$temporary"
302+ fi
303+ chmod 0644 "$temporary"
304+ mv -- "$temporary" "$catalog"
305+ ) 9>"$lock"
306+
307+ - name : Verify the public Catalog and clean server staging
308+ shell : bash
309+ run : |
310+ set -Eeuo pipefail
311+ version='${{ needs.build.outputs.version }}'
312+ curl --fail --silent --show-error --location \
313+ --retry 12 --retry-delay 5 \
314+ "$REGISTRY_BASE_URL/catalog.json?release-run=$GITHUB_RUN_ID" \
315+ --output "$RUNNER_TEMP/catalog.json"
316+ jq -e --arg id "$PACKAGE_ID" --arg version "$version" \
317+ --arg os linux --arg arch x64 \
318+ '.schemaVersion == 1 and
319+ ([.packages[] | select(.packageId == $id)] | length) == 1 and
320+ ((.packages[] | select(.packageId == $id) | .latest[] |
321+ select(.target.os == $os and .target.arch == $arch) | .version) as $current |
322+ ($current == $version or
323+ ([ $current, $version ] | sort_by(split(".") | map(tonumber)) | last) == $current))' \
324+ "$RUNNER_TEMP/catalog.json" >/dev/null
325+
326+ - name : Remove isolated server staging
327+ if : always()
328+ uses : appleboy/ssh-action@v1
329+ with :
330+ host : ${{ secrets.SERVER_HOST }}
331+ username : ${{ secrets.SERVER_USER }}
332+ key : ${{ secrets.SERVER_SSH_KEY }}
333+ script : |
334+ set -eu
335+ staging='/tmp/heartbeat-collector-release-${{ github.run_id }}-${{ github.run_attempt }}'
336+ rm -rf -- "$staging"
0 commit comments