Skip to content

VRChat Collector Release · collector-vrchat/v0.2.1 #3

VRChat Collector Release · collector-vrchat/v0.2.1

VRChat Collector Release · collector-vrchat/v0.2.1 #3

name: Collector Release · VRChat
run-name: VRChat Collector Release · ${{ github.ref_name }}
on:
push:
tags: ['collector-vrchat/v*']
concurrency:
group: collector-vrchat-release-${{ github.ref }}
cancel-in-progress: false
permissions:
contents: read
env:
DOTNET_VERSION: '10.0.x'
PACKAGE_ID: heartbeat.collector.vrchat
REGISTRY_BASE_URL: https://heartbeat.shenxianovo.com/collector-registry/v1
jobs:
build:
name: Build immutable linux-x64 Package release
runs-on: ubuntu-latest
timeout-minutes: 30
outputs:
version: ${{ steps.version.outputs.version }}
artifact_name: ${{ steps.release.outputs.artifact_name }}
steps:
- name: Checkout the tagged commit
uses: actions/checkout@v7
with:
fetch-depth: 0
- name: Validate the dedicated Collector tag
id: version
shell: bash
run: |
set -Eeuo pipefail
if [[ "$GITHUB_REF_NAME" =~ ^collector-vrchat/v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
version="${GITHUB_REF_NAME#collector-vrchat/v}"
else
echo "Expected collector-vrchat/vX.Y.Z, got: $GITHUB_REF_NAME" >&2
exit 1
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
- name: Set up .NET
uses: actions/setup-dotnet@v6
with:
dotnet-version: ${{ env.DOTNET_VERSION }}
- name: Verify VRChat Collector behavior
run: >-
dotnet test
collection/collectors/Heartbeat.Collector.VRChat.Tests/Heartbeat.Collector.VRChat.Tests.csproj
-c Release
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build the tagged VRChat Collector Package
shell: bash
run: |
set -Eeuo pipefail
docker buildx build \
--platform linux/amd64 \
--file collection/collectors/Heartbeat.Collector.VRChat/Dockerfile \
--target package \
--build-arg "COLLECTOR_VERSION=${{ steps.version.outputs.version }}" \
--output type=local,dest=collector-package \
.
- name: Assemble the immutable Web release
id: release
shell: bash
run: |
set -Eeuo pipefail
version='${{ steps.version.outputs.version }}'
./scripts/package-vrchat-release.sh \
--package collector-package \
--version "$version" \
--output release-output \
--base-url "$REGISTRY_BASE_URL"
artifact_name="$PACKAGE_ID-$version-linux-x64.zip"
test -f "release-output/$artifact_name"
test -f release-output/release.json
test -f release-output/catalog-entry.json
test "$(jq -r .packageId release-output/release.json)" = "$PACKAGE_ID"
test "$(jq -r .version release-output/release.json)" = "$version"
test "$(jq -r .artifact.fileName release-output/release.json)" = "$artifact_name"
test "$(jq -r .packageId release-output/catalog-entry.json)" = "$PACKAGE_ID"
test "$(jq -r '.latest | length' release-output/catalog-entry.json)" = 1
test "$(jq -r '.latest[0].version' release-output/catalog-entry.json)" = "$version"
mkdir extracted
unzip -q "release-output/$artifact_name" -d extracted
test -f extracted/collector-manifest.json
test -x extracted/Heartbeat.Collector.VRChat
test "$(jq -r .version extracted/collector-manifest.json)" = "$version"
test "$(jq -r .packageId extracted/collector-manifest.json)" = "$PACKAGE_ID"
test "$(jq -r '.artifacts | length' extracted/collector-manifest.json)" = 1
test "$(jq -r '.artifacts[0].selector.driver' extracted/collector-manifest.json)" = managedProcess
test "$(jq -r '.artifacts[0].selector.os | join(",")' extracted/collector-manifest.json)" = linux
test "$(jq -r '.artifacts[0].selector.arch | join(",")' extracted/collector-manifest.json)" = x64
echo "artifact_name=$artifact_name" >> "$GITHUB_OUTPUT"
- name: Retain the exact release files for diagnosis
uses: actions/upload-artifact@v7
with:
name: collector-vrchat-${{ steps.version.outputs.version }}-linux-x64
path: release-output/*
if-no-files-found: error
compression-level: 0
retention-days: 7
publish:
name: Publish immutable static files
needs: build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- name: Download the exact release files
uses: actions/download-artifact@v8
with:
name: collector-vrchat-${{ needs.build.outputs.version }}-linux-x64
path: release-output
- name: Prepare isolated server staging
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -eu
staging='/tmp/heartbeat-collector-release-${{ github.run_id }}-${{ github.run_attempt }}'
rm -rf -- "$staging"
mkdir -p -- "$staging"
- name: Upload to isolated server staging
uses: appleboy/scp-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
source: release-output
target: /tmp/heartbeat-collector-release-${{ github.run_id }}-${{ github.run_attempt }}
- name: Install the immutable version directory
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -eu
package_id='${{ env.PACKAGE_ID }}'
version='${{ needs.build.outputs.version }}'
artifact='${{ needs.build.outputs.artifact_name }}'
staging='/tmp/heartbeat-collector-release-${{ github.run_id }}-${{ github.run_attempt }}'
source="$staging/release-output"
registry='/srv/heartbeat/collector-registry/v1'
versions="$registry/packages/$package_id/versions"
version_root="$versions/$version"
target="$version_root/linux-x64"
pending="$version_root/.linux-x64.staging-${{ github.run_id }}-${{ github.run_attempt }}"
test -f "$source/$artifact"
test -f "$source/release.json"
install -d -m 0755 -- "$version_root"
if [ -d "$target" ]; then
if cmp -s "$source/$artifact" "$target/$artifact" && \
cmp -s "$source/release.json" "$target/release.json"; then
echo "Exact release already exists; treating rerun as idempotent."
exit 0
fi
echo "Refusing to overwrite $package_id $version with different bytes." >&2
exit 1
fi
rm -rf -- "$pending"
mkdir -- "$pending"
install -m 0644 "$source/$artifact" "$pending/$artifact"
install -m 0644 "$source/release.json" "$pending/release.json"
mv -- "$pending" "$target"
- name: Verify the public exact release bytes
shell: bash
run: |
set -Eeuo pipefail
version='${{ needs.build.outputs.version }}'
artifact='${{ needs.build.outputs.artifact_name }}'
url="$REGISTRY_BASE_URL/packages/$PACKAGE_ID/versions/$version/linux-x64"
curl --fail --silent --show-error --location \
--retry 12 --retry-delay 5 \
"$url/$artifact?release-run=$GITHUB_RUN_ID" \
--output "$RUNNER_TEMP/$artifact"
curl --fail --silent --show-error --location \
--retry 12 --retry-delay 5 \
"$url/release.json?release-run=$GITHUB_RUN_ID" \
--output "$RUNNER_TEMP/release.json"
cmp release-output/$artifact "$RUNNER_TEMP/$artifact"
cmp release-output/release.json "$RUNNER_TEMP/release.json"
- name: Advance the generic Catalog after the exact release is public
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -eu
package_id='${{ env.PACKAGE_ID }}'
version='${{ needs.build.outputs.version }}'
staging='/tmp/heartbeat-collector-release-${{ github.run_id }}-${{ github.run_attempt }}'
entry="$staging/release-output/catalog-entry.json"
registry='/srv/heartbeat/collector-registry/v1'
catalog="$registry/catalog.json"
lock="$registry/.catalog.lock"
command -v jq >/dev/null 2>&1
command -v flock >/dev/null 2>&1
test -f "$entry"
test "$(jq -r .packageId "$entry")" = "$package_id"
test "$(jq -r '.latest | length' "$entry")" = 1
test "$(jq -r '.latest[0].version' "$entry")" = "$version"
target_os=$(jq -r '.latest[0].target.os' "$entry")
target_arch=$(jq -r '.latest[0].target.arch' "$entry")
install -d -m 0755 -- "$registry"
(
flock -x 9
current=''
existing=''
if [ -f "$catalog" ]; then
jq -e '
.schemaVersion == 1 and
(.packages | type == "array") and
([.packages[].packageId] | length == (unique | length)) and
all(.packages[];
(.latest | type == "array") and
([.latest[] | "\(.target.os)/\(.target.arch)"] | length == (unique | length)))
' "$catalog" >/dev/null
existing=$(jq -c --arg id "$package_id" \
'.packages[] | select(.packageId == $id)' "$catalog")
current=$(jq -r --arg id "$package_id" --arg os "$target_os" --arg arch "$target_arch" \
'.packages[] | select(.packageId == $id) | .latest[] |
select(.target.os == $os and .target.arch == $arch) | .version' "$catalog")
fi
if [ -n "$current" ] && [ "$current" != "$version" ] && \
[ "$(printf '%s\n%s\n' "$current" "$version" | sort -V | tail -n 1)" = "$current" ]; then
echo "Catalog already points at newer $package_id $current; leaving it unchanged."
exit 0
fi
if [ "$current" = "$version" ]; then
if jq -e --argjson expected "$(cat "$entry")" --arg id "$package_id" \
--arg os "$target_os" --arg arch "$target_arch" '
.packages[] | select(.packageId == $id) as $actual |
$actual.displayName == $expected.displayName and
$actual.summary == $expected.summary and
($actual.latest[] | select(.target.os == $os and .target.arch == $arch)) == $expected.latest[0]
' "$catalog" >/dev/null; then
echo "Catalog entry already exists; treating rerun as idempotent."
exit 0
fi
echo "Refusing to replace $package_id $target_os/$target_arch $version Catalog metadata with different bytes." >&2
exit 1
fi
incoming=$(cat "$entry")
if [ -n "$existing" ]; then
if ! jq -e --argjson existing "$existing" --argjson incoming "$incoming" \
'$existing.displayName == $incoming.displayName and
$existing.summary == $incoming.summary' >/dev/null; then
echo "Refusing to change shared Catalog presentation while other targets may exist." >&2
exit 1
fi
merged=$(jq -n --argjson existing "$existing" --argjson incoming "$incoming" \
--arg os "$target_os" --arg arch "$target_arch" '
$existing | .latest = (
[.latest[] | select(.target.os != $os or .target.arch != $arch)] +
$incoming.latest |
sort_by(.target.os, .target.arch)
)
')
else
merged=$incoming
fi
temporary="$registry/.catalog.json.${{ github.run_id }}-${{ github.run_attempt }}"
if [ -f "$catalog" ]; then
jq --arg id "$package_id" --argjson merged "$merged" \
'.packages = ([.packages[] | select(.packageId != $id)] + [$merged] | sort_by(.packageId))' \
"$catalog" > "$temporary"
else
jq -n --argjson entry "$merged" \
'{schemaVersion: 1, packages: [$entry]}' > "$temporary"
fi
chmod 0644 "$temporary"
mv -- "$temporary" "$catalog"
) 9>"$lock"
- name: Verify the public Catalog and clean server staging
shell: bash
run: |
set -Eeuo pipefail
version='${{ needs.build.outputs.version }}'
curl --fail --silent --show-error --location \
--retry 12 --retry-delay 5 \
"$REGISTRY_BASE_URL/catalog.json?release-run=$GITHUB_RUN_ID" \
--output "$RUNNER_TEMP/catalog.json"
jq -e --arg id "$PACKAGE_ID" --arg version "$version" \
--arg os linux --arg arch x64 \
'.schemaVersion == 1 and
([.packages[] | select(.packageId == $id)] | length) == 1 and
((.packages[] | select(.packageId == $id) | .latest[] |
select(.target.os == $os and .target.arch == $arch) | .version) as $current |
($current == $version or
([ $current, $version ] | sort_by(split(".") | map(tonumber)) | last) == $current))' \
"$RUNNER_TEMP/catalog.json" >/dev/null
- name: Remove isolated server staging
if: always()
uses: appleboy/ssh-action@v1
with:
host: ${{ secrets.SERVER_HOST }}
username: ${{ secrets.SERVER_USER }}
key: ${{ secrets.SERVER_SSH_KEY }}
script: |
set -eu
staging='/tmp/heartbeat-collector-release-${{ github.run_id }}-${{ github.run_attempt }}'
rm -rf -- "$staging"