mirror of
https://github.com/moeru-ai/airi.git
synced 2026-07-17 03:38:30 +00:00
Some checks are pending
CI / Lint (push) Waiting to run
CI / Build Test (stage-tamagotchi) (push) Waiting to run
CI / Build Test (stage-tamagotchi-godot) (push) Waiting to run
CI / Build Test (stage-web) (push) Waiting to run
CI / Build Test (ui-loading-screens) (push) Waiting to run
CI / Build Test (ui-transitions) (push) Waiting to run
CI / Type Check (push) Waiting to run
CI / Check Provenance (push) Waiting to run
Cloudflare Workers / Deploy - stage-web (push) Waiting to run
Update Nix pnpmDeps Hash / update (push) Waiting to run
## Summary Adds GitCode Release mirroring for AIRI release assets. - Adds a reusable `.github/scripts/publish-gitcode-release.sh` helper that creates or reuses the matching GitCode release, compares GitHub and GitCode asset names, downloads only missing `AIRI-*` and `latest-*.yml` assets, uploads them through GitCode's upload URL API, and verifies the final asset list. - Hooks the mirror step into desktop, Android, and iOS release workflows so assets produced by separate release jobs can converge on the same GitCode release. - Keeps the mirror idempotent so repeated or concurrent release workflows skip already mirrored files. ## Why GitHub Releases are slow for many mainland China users. GitCode provides a domestic fallback release page, but manually copying installers after every release is error-prone and tedious. ## Validation - `bash -n .github/scripts/publish-gitcode-release.sh` - YAML parsing for: - `.github/workflows/release-tamagotchi.yml` - `.github/workflows/release-pocket-android.yml` - `.github/workflows/release-pocket-ios.yml` - `git diff --check -- .github/scripts/publish-gitcode-release.sh .github/workflows/release-tamagotchi.yml .github/workflows/release-pocket-android.yml .github/workflows/release-pocket-ios.yml` - Local idempotency check against existing `v0.10.1` GitHub/GitCode releases: - GitCode release already exists - all mirrored assets already present - final asset verification passed ## Setup Required The target repository needs these GitHub Secrets: - `GITCODE_TOKEN` - `GITCODE_OWNER` - `GITCODE_REPO` For the current GitCode project, `GITCODE_OWNER` should remain `MoeruAI` because GitCode's API and release download paths currently resolve that namespace even though the UI displays `moeru-ai`.
400 lines
12 KiB
Bash
400 lines
12 KiB
Bash
#!/usr/bin/env bash
|
|
|
|
set -euo pipefail
|
|
|
|
GITCODE_API_BASE="${GITCODE_API_BASE:-https://api.gitcode.com/api/v5}"
|
|
GITHUB_REPOSITORY_NAME="${GITHUB_REPOSITORY:-moeru-ai/airi}"
|
|
RELEASE_TAG="${GITCODE_RELEASE_TAG:-${RELEASE_TAG:-${GITHUB_REF_NAME:-}}}"
|
|
GITCODE_TARGET_COMMITISH="${GITCODE_TARGET_COMMITISH:-main}"
|
|
WORK_DIR="${RUNNER_TEMP:-/tmp}/gitcode-release-${RELEASE_TAG}"
|
|
ASSETS_DIR="${WORK_DIR}/assets"
|
|
|
|
if [[ -z "${RELEASE_TAG}" ]]; then
|
|
echo "::error::Missing release tag. Set GITCODE_RELEASE_TAG or run from a release tag ref."
|
|
exit 1
|
|
fi
|
|
|
|
# Ensures required environment variables are present before any network call.
|
|
require_env() {
|
|
local name="$1"
|
|
|
|
if [[ -z "${!name:-}" ]]; then
|
|
echo "::error::Missing required environment variable: ${name}"
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Encodes a single path component for GitCode query parameters.
|
|
urlencode() {
|
|
jq -nr --arg value "$1" '$value | @uri'
|
|
}
|
|
|
|
# Formats a value for curl's config file syntax.
|
|
curl_config_value() {
|
|
jq -Rr @json <<< "$1"
|
|
}
|
|
|
|
# Writes a curl config file for GitCode API calls without putting the token in
|
|
# the process argument list.
|
|
write_gitcode_curl_config() {
|
|
local path="$1"
|
|
local config_file="$2"
|
|
|
|
{
|
|
printf 'url = %s\n' "$(curl_config_value "${GITCODE_API_BASE}${path}")"
|
|
printf 'url-query = %s\n' "$(curl_config_value "access_token=${GITCODE_TOKEN}")"
|
|
} > "${config_file}"
|
|
|
|
chmod 600 "${config_file}"
|
|
}
|
|
|
|
# Reads GitHub release metadata and creates the same GitCode release when absent.
|
|
ensure_gitcode_release() {
|
|
local github_release_json="${WORK_DIR}/github-release.json"
|
|
local gitcode_release_json="${WORK_DIR}/gitcode-release.json"
|
|
local response_json="${WORK_DIR}/create-release-response.json"
|
|
local request_config="${WORK_DIR}/create-release-request.curlrc"
|
|
local http_status
|
|
|
|
gh release view "${RELEASE_TAG}" \
|
|
--repo "${GITHUB_REPOSITORY_NAME}" \
|
|
--json tagName,name,body,isPrerelease \
|
|
> "${github_release_json}"
|
|
|
|
jq --arg target_commitish "${GITCODE_TARGET_COMMITISH}" '{
|
|
tag_name: .tagName,
|
|
name: .name,
|
|
body: .body,
|
|
target_commitish: $target_commitish,
|
|
release_status: (if .isPrerelease then "pre" else "latest" end)
|
|
}' "${github_release_json}" > "${gitcode_release_json}"
|
|
|
|
write_gitcode_curl_config "/repos/${GITCODE_OWNER}/${GITCODE_REPO}/releases" "${request_config}"
|
|
|
|
http_status="$(
|
|
curl --retry 3 --retry-all-errors -sS \
|
|
--config "${request_config}" \
|
|
-o "${response_json}" \
|
|
-w '%{http_code}' \
|
|
-X POST \
|
|
-H 'Content-Type: application/json' \
|
|
--data @"${gitcode_release_json}"
|
|
)"
|
|
|
|
if [[ "${http_status}" =~ ^2 ]]; then
|
|
echo "Created GitCode release ${RELEASE_TAG}."
|
|
return
|
|
fi
|
|
|
|
if jq -e '.error_code == 409 or (.error_message // "" | contains("Release already exists"))' "${response_json}" >/dev/null 2>&1; then
|
|
echo "GitCode release ${RELEASE_TAG} already exists; reusing it."
|
|
return
|
|
fi
|
|
|
|
echo "::error::Failed to create GitCode release ${RELEASE_TAG}; HTTP ${http_status}."
|
|
exit 1
|
|
}
|
|
|
|
# Fetches the current GitCode release assets so repeated workflow runs can skip
|
|
# files that were already mirrored.
|
|
write_existing_gitcode_asset_names() {
|
|
local release_json="${WORK_DIR}/gitcode-release-current.json"
|
|
local request_config="${WORK_DIR}/get-release-request.curlrc"
|
|
local encoded_tag
|
|
|
|
encoded_tag="$(urlencode "${RELEASE_TAG}")"
|
|
|
|
write_gitcode_curl_config "/repos/${GITCODE_OWNER}/${GITCODE_REPO}/releases/tags/${encoded_tag}" "${request_config}"
|
|
|
|
curl --retry 3 --retry-all-errors -fsS \
|
|
--config "${request_config}" \
|
|
> "${release_json}"
|
|
|
|
jq -r '.assets[]?.name' "${release_json}" | sort > "${WORK_DIR}/existing-gitcode-assets.txt"
|
|
}
|
|
|
|
# Writes the GitHub release assets that should be mirrored to GitCode.
|
|
write_expected_github_asset_metadata() {
|
|
local release_assets_json="${WORK_DIR}/github-release-assets.json"
|
|
local encoded_tag
|
|
|
|
encoded_tag="$(urlencode "${RELEASE_TAG}")"
|
|
|
|
gh api "repos/${GITHUB_REPOSITORY_NAME}/releases/tags/${encoded_tag}" \
|
|
> "${release_assets_json}"
|
|
|
|
jq -r '.assets[].name | select(test("^(AIRI-.*|latest-.*\\.yml)$"))' "${release_assets_json}" \
|
|
| sort \
|
|
> "${WORK_DIR}/expected-asset-names.txt"
|
|
|
|
jq -r '.assets[] | select(.name | test("^(AIRI-.*|latest-.*\\.yml)$")) | [.name, (.digest // "")] | @tsv' "${release_assets_json}" \
|
|
| sort \
|
|
> "${WORK_DIR}/expected-asset-digests.tsv"
|
|
|
|
if [[ ! -s "${WORK_DIR}/expected-asset-names.txt" ]]; then
|
|
echo "::error::No GitHub release assets matched AIRI-* or latest-*.yml."
|
|
exit 1
|
|
fi
|
|
|
|
if awk -F '\t' '$2 == "" { found = 1 } END { exit found ? 0 : 1 }' "${WORK_DIR}/expected-asset-digests.tsv"; then
|
|
echo "::error::GitHub release assets are missing SHA-256 digests; cannot verify GitCode mirror freshness."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Builds the public GitCode release download URL for an uploaded asset.
|
|
gitcode_download_url() {
|
|
local filename="$1"
|
|
local encoded_filename
|
|
local encoded_tag
|
|
|
|
encoded_filename="$(urlencode "${filename}")"
|
|
encoded_tag="$(urlencode "${RELEASE_TAG}")"
|
|
|
|
printf 'https://gitcode.com/%s/%s/releases/download/%s/%s' \
|
|
"${GITCODE_OWNER}" \
|
|
"${GITCODE_REPO}" \
|
|
"${encoded_tag}" \
|
|
"${encoded_filename}"
|
|
}
|
|
|
|
# Reads a GitCode asset SHA-256 digest by streaming the public download.
|
|
read_gitcode_asset_digest() {
|
|
local filename="$1"
|
|
local download_url
|
|
|
|
download_url="$(gitcode_download_url "${filename}")"
|
|
|
|
curl --retry 3 --retry-all-errors -fsSL "${download_url}" \
|
|
| sha256sum \
|
|
| awk '{ print "sha256:" $1 }'
|
|
}
|
|
|
|
# Verifies same-name GitCode assets still match the GitHub release asset digests.
|
|
verify_gitcode_asset_digests() {
|
|
local mode="${1:-strict}"
|
|
local stale_assets="${WORK_DIR}/stale-gitcode-assets.tsv"
|
|
local stale_names="${WORK_DIR}/stale-gitcode-asset-names.txt"
|
|
local filename
|
|
local expected_digest
|
|
local actual_digest
|
|
|
|
: > "${stale_assets}"
|
|
: > "${stale_names}"
|
|
|
|
while IFS=$'\t' read -r filename expected_digest; do
|
|
if ! grep -Fxq "${filename}" "${WORK_DIR}/existing-gitcode-assets.txt"; then
|
|
continue
|
|
fi
|
|
|
|
if ! actual_digest="$(read_gitcode_asset_digest "${filename}")"; then
|
|
echo "::error::Could not read GitCode asset digest for ${filename}."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ -z "${actual_digest}" ]]; then
|
|
echo "::error::Could not read GitCode asset digest for ${filename}."
|
|
exit 1
|
|
fi
|
|
|
|
if [[ "${actual_digest}" != "${expected_digest}" ]]; then
|
|
printf '%s\t%s\t%s\n' "${filename}" "${expected_digest}" "${actual_digest}" >> "${stale_assets}"
|
|
printf '%s\n' "${filename}" >> "${stale_names}"
|
|
fi
|
|
done < "${WORK_DIR}/expected-asset-digests.tsv"
|
|
|
|
if [[ -s "${stale_assets}" ]]; then
|
|
if [[ "${mode}" == "warn" ]]; then
|
|
echo "::warning::GitCode release contains stale mirrored assets; they will be uploaded again:"
|
|
else
|
|
echo "::error::GitCode release contains stale mirrored assets with digest mismatches:"
|
|
fi
|
|
|
|
awk -F '\t' '{ printf " - %s: GitHub=%s, GitCode=%s\n", $1, $2, $3 }' "${stale_assets}"
|
|
|
|
if [[ "${mode}" == "warn" ]]; then
|
|
sort -u -o "${stale_names}" "${stale_names}"
|
|
return
|
|
fi
|
|
|
|
echo "::error::GitCode release still has stale mirrored assets after upload."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Checks whether an existing GitCode asset should be replaced by the GitHub one.
|
|
is_stale_gitcode_asset() {
|
|
local filename="$1"
|
|
local stale_names="${WORK_DIR}/stale-gitcode-asset-names.txt"
|
|
|
|
[[ -s "${stale_names}" ]] && grep -Fxq "${filename}" "${stale_names}"
|
|
}
|
|
|
|
# Downloads GitHub release assets that are missing from GitCode or stale there.
|
|
download_missing_github_release_assets() {
|
|
local missing_names="${WORK_DIR}/missing-before-upload.txt"
|
|
local stale_names="${WORK_DIR}/stale-gitcode-asset-names.txt"
|
|
local upload_names="${WORK_DIR}/asset-names-to-upload.txt"
|
|
|
|
rm -rf "${ASSETS_DIR}"
|
|
mkdir -p "${ASSETS_DIR}"
|
|
|
|
comm -23 "${WORK_DIR}/expected-asset-names.txt" "${WORK_DIR}/existing-gitcode-assets.txt" > "${missing_names}"
|
|
{
|
|
cat "${missing_names}"
|
|
if [[ -s "${stale_names}" ]]; then
|
|
cat "${stale_names}"
|
|
fi
|
|
} | sort -u > "${upload_names}"
|
|
|
|
if [[ ! -s "${upload_names}" ]]; then
|
|
echo "GitCode release ${RELEASE_TAG} already has every mirrored asset."
|
|
: > "${WORK_DIR}/assets-to-upload.txt"
|
|
return
|
|
fi
|
|
|
|
while IFS= read -r filename; do
|
|
gh release download "${RELEASE_TAG}" \
|
|
--repo "${GITHUB_REPOSITORY_NAME}" \
|
|
--pattern "${filename}" \
|
|
--dir "${ASSETS_DIR}" \
|
|
--clobber
|
|
done < "${upload_names}"
|
|
|
|
find "${ASSETS_DIR}" -maxdepth 1 -type f -print | sort > "${WORK_DIR}/assets-to-upload.txt"
|
|
}
|
|
|
|
# Uploads one local asset through GitCode's pre-signed OBS upload URL.
|
|
upload_asset() {
|
|
local file="$1"
|
|
local filename
|
|
local encoded_filename
|
|
local upload_json
|
|
local upload_url
|
|
local upload_url_status
|
|
local response_body
|
|
local http_status
|
|
local x_obs_acl
|
|
local x_obs_callback
|
|
local x_obs_meta_project_id
|
|
local x_obs_content_type
|
|
local request_config
|
|
local encoded_tag
|
|
|
|
filename="$(basename "${file}")"
|
|
|
|
if grep -Fxq "${filename}" "${WORK_DIR}/existing-gitcode-assets.txt" && ! is_stale_gitcode_asset "${filename}"; then
|
|
echo "Skipping existing GitCode asset: ${filename}"
|
|
return
|
|
fi
|
|
|
|
echo "Uploading GitCode asset: ${filename}"
|
|
|
|
encoded_filename="$(urlencode "${filename}")"
|
|
encoded_tag="$(urlencode "${RELEASE_TAG}")"
|
|
upload_json="${WORK_DIR}/upload-${filename}.json"
|
|
response_body="${WORK_DIR}/upload-${filename}.response"
|
|
request_config="${WORK_DIR}/upload-url-${filename}.curlrc"
|
|
|
|
write_gitcode_curl_config "/repos/${GITCODE_OWNER}/${GITCODE_REPO}/releases/${encoded_tag}/upload_url?file_name=${encoded_filename}" "${request_config}"
|
|
|
|
upload_url_status="$(
|
|
curl --retry 3 --retry-all-errors -sS \
|
|
--config "${request_config}" \
|
|
-o "${upload_json}" \
|
|
-w '%{http_code}'
|
|
)"
|
|
|
|
if [[ ! "${upload_url_status}" =~ ^2 ]]; then
|
|
write_existing_gitcode_asset_names
|
|
if grep -Fxq "${filename}" "${WORK_DIR}/existing-gitcode-assets.txt" && ! is_stale_gitcode_asset "${filename}"; then
|
|
echo "Skipping concurrently uploaded GitCode asset: ${filename}"
|
|
return
|
|
fi
|
|
|
|
echo "::error::Failed to prepare GitCode upload URL for ${filename}; HTTP ${upload_url_status}."
|
|
exit 1
|
|
fi
|
|
|
|
{
|
|
IFS=$'\t' read -r upload_url x_obs_meta_project_id x_obs_acl x_obs_callback x_obs_content_type
|
|
} < <(
|
|
jq -r '[
|
|
.url,
|
|
.headers["x-obs-meta-project-id"],
|
|
.headers["x-obs-acl"],
|
|
.headers["x-obs-callback"],
|
|
.headers["Content-Type"]
|
|
] | @tsv' "${upload_json}"
|
|
)
|
|
|
|
http_status="$(
|
|
curl --retry 3 --retry-all-errors -sS \
|
|
-o "${response_body}" \
|
|
-w '%{http_code}' \
|
|
-X PUT \
|
|
-H "x-obs-meta-project-id: ${x_obs_meta_project_id}" \
|
|
-H "x-obs-acl: ${x_obs_acl}" \
|
|
-H "x-obs-callback: ${x_obs_callback}" \
|
|
-H "Content-Type: ${x_obs_content_type}" \
|
|
--upload-file "${file}" \
|
|
"${upload_url}"
|
|
)"
|
|
|
|
if [[ ! "${http_status}" =~ ^2 ]]; then
|
|
write_existing_gitcode_asset_names
|
|
if grep -Fxq "${filename}" "${WORK_DIR}/existing-gitcode-assets.txt" && ! is_stale_gitcode_asset "${filename}"; then
|
|
echo "GitCode asset appeared after upload retry: ${filename}"
|
|
return
|
|
fi
|
|
|
|
echo "::error::Failed to upload ${filename} to GitCode; HTTP ${http_status}."
|
|
exit 1
|
|
fi
|
|
}
|
|
|
|
# Verifies every downloaded GitHub release asset is visible in the GitCode
|
|
# release after upload.
|
|
verify_gitcode_release_assets() {
|
|
local current_names="${WORK_DIR}/gitcode-asset-names-after-upload.txt"
|
|
local missing_names="${WORK_DIR}/missing-gitcode-assets.txt"
|
|
|
|
write_existing_gitcode_asset_names
|
|
|
|
cp "${WORK_DIR}/existing-gitcode-assets.txt" "${current_names}"
|
|
comm -23 "${WORK_DIR}/expected-asset-names.txt" "${current_names}" > "${missing_names}"
|
|
|
|
if [[ -s "${missing_names}" ]]; then
|
|
echo "::error::GitCode release is missing mirrored assets:"
|
|
sed 's/^/ - /' "${missing_names}"
|
|
exit 1
|
|
fi
|
|
|
|
verify_gitcode_asset_digests
|
|
|
|
echo "GitCode release ${RELEASE_TAG} contains all mirrored assets."
|
|
}
|
|
|
|
# Runs the full GitHub Release to GitCode Release mirror flow.
|
|
main() {
|
|
require_env GH_TOKEN
|
|
require_env GITCODE_TOKEN
|
|
require_env GITCODE_OWNER
|
|
require_env GITCODE_REPO
|
|
|
|
mkdir -p "${WORK_DIR}"
|
|
|
|
ensure_gitcode_release
|
|
write_existing_gitcode_asset_names
|
|
write_expected_github_asset_metadata
|
|
verify_gitcode_asset_digests warn
|
|
download_missing_github_release_assets
|
|
|
|
while IFS= read -r file; do
|
|
upload_asset "${file}"
|
|
done < "${WORK_DIR}/assets-to-upload.txt"
|
|
|
|
verify_gitcode_release_assets
|
|
}
|
|
|
|
main "$@"
|