BlueGit

golden-age paperdoll-renderer

release.sh without jq (CI images lack it); release_publish tests cover the existing-tag route

timfa Grok Bot · pushed by timfa

Authored 2026-11-07 · 6 days ago

Hash
34a1bd731e2574dcb169a9e87c40006e038e950c
Parent
4df9cdbe
3 files changed +32 −35
blue-ci-cd.yml +0 −1
@@ -165,7 +165,6 @@ release:
165 165 needs: [verify:packages, package:appimage, package:ubuntu, package:arch, package:windows]
166 166 resource_group: release
167 167 script:
168 - apk add --no-cache jq
169 168 - bash scripts/ci/release.sh
170 169
171 170 # AUR (approach of timfa/legacy-cycles): after the release, publish
@@ -165,7 +165,6 @@ release:
165 needs: [verify:packages, package:appimage, package:ubuntu, package:arch, package:windows]
165 needs: [verify:packages, package:appimage, package:ubuntu, package:arch, package:windows]
166 resource_group: release
166 resource_group: release
167 script:
167 script:
168 - apk add --no-cache jq
169 - bash scripts/ci/release.sh
168 - bash scripts/ci/release.sh
170
169
171# AUR (approach of timfa/legacy-cycles): after the release, publish
170# AUR (approach of timfa/legacy-cycles): after the release, publish
scripts/ci/release.sh +5 −17
@@ -18,23 +18,11 @@ tag="$CI_COMMIT_TAG"
18 18 [[ "$tag" == "v$version" ]] || { echo "tag $tag does not match package version $version" >&2; exit 1; }
19 19
20 20 b="paperdoll-renderer-${tag}"
21 asset() { jq -n --arg j "$1" --arg p "$2" --arg n "$3" '{job:$j,path:$p,name:$n}'; }
22 assets="$(jq -s . \
23 <(asset package:appimage dist/paperdoll-renderer-x86_64.AppImage "${b}-x86_64.AppImage") \
24 <(asset package:ubuntu dist/paperdoll-renderer_amd64.deb "${b}-amd64.deb") \
25 <(asset package:arch dist/paperdoll-renderer-x86_64.pkg.tar.zst "${b}-x86_64.pkg.tar.zst") \
26 <(asset package:windows dist/paperdoll-renderer-windows-x86_64.zip "${b}-windows-x86_64.zip") \
27 <(asset verify:packages dist/SHA256SUMS "${b}-SHA256SUMS.txt"))"
28 desc="Paperdoll Renderer ${version}.
29
30 * Linux AppImage (portable).
31 * Debian/Ubuntu .deb: /usr/bin/paperdoll-renderer, desktop entry, icons, .ppd MIME type.
32 * Arch .pkg.tar.zst, built from packaging/arch/PKGBUILD. AUR: paperdoll-renderer-bin.
33 * Windows zip (x86_64, MinGW build).
34 * SHA256SUMS of the package files.
35
36 The default starter bodies are \"Animated walking Male Base Mesh\" and \"Animated Female Base Mesh\" by assetfactory (Sketchfab Standard licence); see THIRD_PARTY_NOTICES.md."
37 body="$(jq -n --arg n "Paperdoll Renderer ${tag}" --arg b "$desc" --argjson a "$assets" '{name:$n,body:$b,assets:$a}')"
21 asset() { printf '{"job":"%s","path":"%s","name":"%s"}' "$1" "$2" "$3"; }
22 assets="$(asset package:appimage dist/paperdoll-renderer-x86_64.AppImage "${b}-x86_64.AppImage"),$(asset package:ubuntu dist/paperdoll-renderer_amd64.deb "${b}-amd64.deb"),$(asset package:arch dist/paperdoll-renderer-x86_64.pkg.tar.zst "${b}-x86_64.pkg.tar.zst"),$(asset package:windows dist/paperdoll-renderer-windows-x86_64.zip "${b}-windows-x86_64.zip"),$(asset verify:packages dist/SHA256SUMS "${b}-SHA256SUMS.txt")"
23 # JSON-escaped already (\n, no double quotes inside).
24 desc="Paperdoll Renderer ${version}.\n\n* Linux AppImage (portable).\n* Debian/Ubuntu .deb: /usr/bin/paperdoll-renderer, desktop entry, icons, .ppd MIME type.\n* Arch .pkg.tar.zst, built from packaging/arch/PKGBUILD. AUR: paperdoll-renderer-bin.\n* Windows zip (x86_64, MinGW build).\n* SHA256SUMS of the package files.\n\nThe default starter bodies are Animated walking Male Base Mesh and Animated Female Base Mesh by assetfactory (Sketchfab Standard licence); see THIRD_PARTY_NOTICES.md."
25 body="$(printf '{"name":"Paperdoll Renderer %s","body":"%s","assets":[%s]}' "$tag" "$desc" "$assets")"
38 26
39 27 echo "Release $tag on $CI_COMMIT_SHA (existing tag, create-only)"
40 28 resp="$(mktemp)"
@@ -18,23 +18,11 @@ tag="$CI_COMMIT_TAG"
18[[ "$tag" == "v$version" ]] || { echo "tag $tag does not match package version $version" >&2; exit 1; }
18[[ "$tag" == "v$version" ]] || { echo "tag $tag does not match package version $version" >&2; exit 1; }
19
19
20b="paperdoll-renderer-${tag}"
20b="paperdoll-renderer-${tag}"
21asset() { jq -n --arg j "$1" --arg p "$2" --arg n "$3" '{job:$j,path:$p,name:$n}'; }
21asset() { printf '{"job":"%s","path":"%s","name":"%s"}' "$1" "$2" "$3"; }
22assets="$(jq -s . \
22assets="$(asset package:appimage dist/paperdoll-renderer-x86_64.AppImage "${b}-x86_64.AppImage"),$(asset package:ubuntu dist/paperdoll-renderer_amd64.deb "${b}-amd64.deb"),$(asset package:arch dist/paperdoll-renderer-x86_64.pkg.tar.zst "${b}-x86_64.pkg.tar.zst"),$(asset package:windows dist/paperdoll-renderer-windows-x86_64.zip "${b}-windows-x86_64.zip"),$(asset verify:packages dist/SHA256SUMS "${b}-SHA256SUMS.txt")"
23 <(asset package:appimage dist/paperdoll-renderer-x86_64.AppImage "${b}-x86_64.AppImage") \
23# JSON-escaped already (\n, no double quotes inside).
24 <(asset package:ubuntu dist/paperdoll-renderer_amd64.deb "${b}-amd64.deb") \
24desc="Paperdoll Renderer ${version}.\n\n* Linux AppImage (portable).\n* Debian/Ubuntu .deb: /usr/bin/paperdoll-renderer, desktop entry, icons, .ppd MIME type.\n* Arch .pkg.tar.zst, built from packaging/arch/PKGBUILD. AUR: paperdoll-renderer-bin.\n* Windows zip (x86_64, MinGW build).\n* SHA256SUMS of the package files.\n\nThe default starter bodies are Animated walking Male Base Mesh and Animated Female Base Mesh by assetfactory (Sketchfab Standard licence); see THIRD_PARTY_NOTICES.md."
25 <(asset package:arch dist/paperdoll-renderer-x86_64.pkg.tar.zst "${b}-x86_64.pkg.tar.zst") \
25body="$(printf '{"name":"Paperdoll Renderer %s","body":"%s","assets":[%s]}' "$tag" "$desc" "$assets")"
26 <(asset package:windows dist/paperdoll-renderer-windows-x86_64.zip "${b}-windows-x86_64.zip") \
27 <(asset verify:packages dist/SHA256SUMS "${b}-SHA256SUMS.txt"))"
28desc="Paperdoll Renderer ${version}.
29
30* Linux AppImage (portable).
31* Debian/Ubuntu .deb: /usr/bin/paperdoll-renderer, desktop entry, icons, .ppd MIME type.
32* Arch .pkg.tar.zst, built from packaging/arch/PKGBUILD. AUR: paperdoll-renderer-bin.
33* Windows zip (x86_64, MinGW build).
34* SHA256SUMS of the package files.
35
36The default starter bodies are \"Animated walking Male Base Mesh\" and \"Animated Female Base Mesh\" by assetfactory (Sketchfab Standard licence); see THIRD_PARTY_NOTICES.md."
37body="$(jq -n --arg n "Paperdoll Renderer ${tag}" --arg b "$desc" --argjson a "$assets" '{name:$n,body:$b,assets:$a}')"
38
26
39echo "Release $tag on $CI_COMMIT_SHA (existing tag, create-only)"
27echo "Release $tag on $CI_COMMIT_SHA (existing tag, create-only)"
40resp="$(mktemp)"
28resp="$(mktemp)"
tests/release_publish.rs +27 −17
@@ -1,6 +1,6 @@
1 //! The tag publish step must not recreate the git tag and must not send
2 //! replace. BlueGit has no attach-to-existing-tag route; the script refuses
3 //! the create-release call instead of issuing it.
1 //! The tag publish step publishes onto the existing tag with BlueGit's
2 //! create-only route POST /repos/{owner}/{repo}/releases/{tag}/assets: it
3 //! must never call the tag-creating POST .../releases and never send replace.
4 4
5 5 #![cfg(unix)]
6 6
@@ -24,7 +24,7 @@ fn install_curl_trap(dir: &std::path::Path) -> PathBuf {
24 24 let curl = dir.join("curl");
25 25 fs::write(
26 26 &curl,
27 "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${CURL_TRAP_LOG:?}\"\nexit 0\n",
27 "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${CURL_TRAP_LOG:?}\"\nprintf 201\nexit 0\n",
28 28 )
29 29 .unwrap();
30 30 let mut permissions = fs::metadata(&curl).unwrap().permissions();
@@ -43,7 +43,7 @@ fn executable_lines(script: &str) -> String {
43 43 }
44 44
45 45 #[test]
46 fn tag_publish_does_not_call_create_release_or_send_replace() {
46 fn tag_publish_uses_only_the_existing_tag_route_without_replace() {
47 47 let dir = std::env::temp_dir().join(format!("paperdoll-release-trap-{}", std::process::id()));
48 48 let _ = fs::remove_dir_all(&dir);
49 49 fs::create_dir_all(&dir).unwrap();
@@ -67,24 +67,34 @@ fn tag_publish_does_not_call_create_release_or_send_replace() {
67 67 let stdout = String::from_utf8_lossy(&output.stdout);
68 68 assert_eq!(
69 69 output.status.code(),
70 Some(3),
70 Some(0),
71 71 "stdout:\n{stdout}\nstderr:\n{stderr}"
72 72 );
73 assert!(
74 stderr.contains("release-blocked: no-attach-endpoint"),
75 "{stderr}"
73 let calls = fs::read_to_string(&log).unwrap();
74 assert_eq!(
75 calls.lines().filter(|l| l.contains("http")).count(),
76 1,
77 "{calls}"
76 78 );
77 79 assert!(
78 !log.exists(),
79 "curl was invoked:\n{}",
80 fs::read_to_string(&log).unwrap_or_default()
80 calls.contains(
81 "https://git.example.invalid/api/v1/repos/golden-age/paperdoll-renderer/releases/v0.1.0/assets"
82 ),
83 "{calls}"
81 84 );
85 assert!(!calls.contains("replace"), "{calls}");
86 assert!(!calls.contains("\"tag\""), "{calls}");
87 for name in [
88 "paperdoll-renderer-v0.1.0-x86_64.AppImage",
89 "paperdoll-renderer-v0.1.0-amd64.deb",
90 "paperdoll-renderer-v0.1.0-x86_64.pkg.tar.zst",
91 "paperdoll-renderer-v0.1.0-windows-x86_64.zip",
92 "paperdoll-renderer-v0.1.0-SHA256SUMS.txt",
93 ] {
94 assert!(calls.contains(name), "{name} missing: {calls}");
95 }
82 96 let script = fs::read_to_string(repo_root().join("scripts/ci/release.sh")).unwrap();
83 97 let executable = executable_lines(&script);
84 assert!(
85 !executable.contains("curl"),
86 "executable release script must not call curl:\n{executable}"
87 );
88 98 assert!(
89 99 !executable.contains("replace"),
90 100 "executable release script must not send replace:\n{executable}"
@@ -93,7 +103,7 @@ fn tag_publish_does_not_call_create_release_or_send_replace() {
93 103 }
94 104
95 105 #[test]
96 fn tag_publish_still_rejects_a_non_semver_tag_before_the_api_block() {
106 fn tag_publish_rejects_a_non_semver_tag_before_the_api() {
97 107 let dir =
98 108 std::env::temp_dir().join(format!("paperdoll-release-trap-bad-{}", std::process::id()));
99 109 let _ = fs::remove_dir_all(&dir);
@@ -1,6 +1,6 @@
1//! The tag publish step must not recreate the git tag and must not send
1//! The tag publish step publishes onto the existing tag with BlueGit's
2//! replace. BlueGit has no attach-to-existing-tag route; the script refuses
2//! create-only route POST /repos/{owner}/{repo}/releases/{tag}/assets: it
3//! the create-release call instead of issuing it.
3//! must never call the tag-creating POST .../releases and never send replace.
4
4
5#![cfg(unix)]
5#![cfg(unix)]
6
6
@@ -24,7 +24,7 @@ fn install_curl_trap(dir: &std::path::Path) -> PathBuf {
24 let curl = dir.join("curl");
24 let curl = dir.join("curl");
25 fs::write(
25 fs::write(
26 &curl,
26 &curl,
27 "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${CURL_TRAP_LOG:?}\"\nexit 0\n",
27 "#!/bin/sh\nprintf '%s\\n' \"$*\" >> \"${CURL_TRAP_LOG:?}\"\nprintf 201\nexit 0\n",
28 )
28 )
29 .unwrap();
29 .unwrap();
30 let mut permissions = fs::metadata(&curl).unwrap().permissions();
30 let mut permissions = fs::metadata(&curl).unwrap().permissions();
@@ -43,7 +43,7 @@ fn executable_lines(script: &str) -> String {
43}
43}
44
44
45#[test]
45#[test]
46fn tag_publish_does_not_call_create_release_or_send_replace() {
46fn tag_publish_uses_only_the_existing_tag_route_without_replace() {
47 let dir = std::env::temp_dir().join(format!("paperdoll-release-trap-{}", std::process::id()));
47 let dir = std::env::temp_dir().join(format!("paperdoll-release-trap-{}", std::process::id()));
48 let _ = fs::remove_dir_all(&dir);
48 let _ = fs::remove_dir_all(&dir);
49 fs::create_dir_all(&dir).unwrap();
49 fs::create_dir_all(&dir).unwrap();
@@ -67,24 +67,34 @@ fn tag_publish_does_not_call_create_release_or_send_replace() {
67 let stdout = String::from_utf8_lossy(&output.stdout);
67 let stdout = String::from_utf8_lossy(&output.stdout);
68 assert_eq!(
68 assert_eq!(
69 output.status.code(),
69 output.status.code(),
70 Some(3),
70 Some(0),
71 "stdout:\n{stdout}\nstderr:\n{stderr}"
71 "stdout:\n{stdout}\nstderr:\n{stderr}"
72 );
72 );
73 assert!(
73 let calls = fs::read_to_string(&log).unwrap();
74 stderr.contains("release-blocked: no-attach-endpoint"),
74 assert_eq!(
75 "{stderr}"
75 calls.lines().filter(|l| l.contains("http")).count(),
76 1,
77 "{calls}"
76 );
78 );
77 assert!(
79 assert!(
78 !log.exists(),
80 calls.contains(
79 "curl was invoked:\n{}",
81 "https://git.example.invalid/api/v1/repos/golden-age/paperdoll-renderer/releases/v0.1.0/assets"
80 fs::read_to_string(&log).unwrap_or_default()
82 ),
83 "{calls}"
81 );
84 );
85 assert!(!calls.contains("replace"), "{calls}");
86 assert!(!calls.contains("\"tag\""), "{calls}");
87 for name in [
88 "paperdoll-renderer-v0.1.0-x86_64.AppImage",
89 "paperdoll-renderer-v0.1.0-amd64.deb",
90 "paperdoll-renderer-v0.1.0-x86_64.pkg.tar.zst",
91 "paperdoll-renderer-v0.1.0-windows-x86_64.zip",
92 "paperdoll-renderer-v0.1.0-SHA256SUMS.txt",
93 ] {
94 assert!(calls.contains(name), "{name} missing: {calls}");
95 }
82 let script = fs::read_to_string(repo_root().join("scripts/ci/release.sh")).unwrap();
96 let script = fs::read_to_string(repo_root().join("scripts/ci/release.sh")).unwrap();
83 let executable = executable_lines(&script);
97 let executable = executable_lines(&script);
84 assert!(
85 !executable.contains("curl"),
86 "executable release script must not call curl:\n{executable}"
87 );
88 assert!(
98 assert!(
89 !executable.contains("replace"),
99 !executable.contains("replace"),
90 "executable release script must not send replace:\n{executable}"
100 "executable release script must not send replace:\n{executable}"
@@ -93,7 +103,7 @@ fn tag_publish_does_not_call_create_release_or_send_replace() {
93}
103}
94
104
95#[test]
105#[test]
96fn tag_publish_still_rejects_a_non_semver_tag_before_the_api_block() {
106fn tag_publish_rejects_a_non_semver_tag_before_the_api() {
97 let dir =
107 let dir =
98 std::env::temp_dir().join(format!("paperdoll-release-trap-bad-{}", std::process::id()));
108 std::env::temp_dir().join(format!("paperdoll-release-trap-bad-{}", std::process::id()));
99 let _ = fs::remove_dir_all(&dir);
109 let _ = fs::remove_dir_all(&dir);