Extract release source archive packaging

This commit is contained in:
m1ngsama 2026-05-28 15:19:20 +08:00
parent 885ba9f6f9
commit a51d0fb605
10 changed files with 243 additions and 34 deletions

View file

@ -109,16 +109,8 @@ jobs:
- name: Build source archive - name: Build source archive
run: | run: |
set -eu archive=$(scripts/package_source_archive.sh "${GITHUB_REF_NAME}" dist)
version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h) sha256sum "$archive"
commit=$(git rev-list -n 1 "${GITHUB_REF_NAME}")
mkdir -p dist
git archive --format=tar.gz --prefix="TNT-${version}/" \
-o "dist/tnt-chat-v${version}-source.tar.gz" "$commit"
tar -tzf "dist/tnt-chat-v${version}-source.tar.gz" >/dev/null
tar -tzf "dist/tnt-chat-v${version}-source.tar.gz" | grep -q "^TNT-${version}/LICENSE$"
tar -tzf "dist/tnt-chat-v${version}-source.tar.gz" | grep -q "^TNT-${version}/packaging/README.md$"
sha256sum "dist/tnt-chat-v${version}-source.tar.gz"
- name: Upload source archive - name: Upload source archive
uses: actions/upload-artifact@v4 uses: actions/upload-artifact@v4

View file

@ -134,6 +134,7 @@ script-test: all
@cd tests && ./test_docs_help_surface.sh @cd tests && ./test_docs_help_surface.sh
@cd tests && ./test_logrotate.sh @cd tests && ./test_logrotate.sh
@cd tests && ./test_message_log_tool.sh @cd tests && ./test_message_log_tool.sh
@cd tests && ./test_source_archive.sh
@cd tests && ./test_release_artifact_gate.sh @cd tests && ./test_release_artifact_gate.sh
integration-test: all integration-test: all

View file

@ -12,6 +12,9 @@
- Added CI governance layers for fast PR checks, release-branch validation, - Added CI governance layers for fast PR checks, release-branch validation,
extended runtime validation, container portability builds, and package recipe extended runtime validation, container portability builds, and package recipe
validation. validation.
- Added `scripts/package_source_archive.sh` so the explicit release source
archive can be built and tested locally instead of living only inside the
GitHub release workflow.
- Added a `config_defaults` module and unit coverage for runtime default - Added a `config_defaults` module and unit coverage for runtime default
values, env keys, and accepted numeric ranges. values, env keys, and accepted numeric ranges.
- Added a dedicated `tntctl_text` module with unit coverage for local - Added a dedicated `tntctl_text` module with unit coverage for local

View file

@ -67,7 +67,8 @@ Runs only for SemVer tags matching `vMAJOR.MINOR.PATCH`:
- Builds Linux glibc AMD64 and ARM64 binaries. - Builds Linux glibc AMD64 and ARM64 binaries.
- Builds macOS Intel and Apple Silicon binaries. - Builds macOS Intel and Apple Silicon binaries.
- Verifies binary architecture labels. - Verifies binary architecture labels.
- Builds an explicit source archive: `tnt-chat-vX.Y.Z-source.tar.gz`. - Builds an explicit source archive with `scripts/package_source_archive.sh`:
`tnt-chat-vX.Y.Z-source.tar.gz`.
- Runs `scripts/package_release_assets.sh` to collect release assets, verify - Runs `scripts/package_release_assets.sh` to collect release assets, verify
expected asset names, verify binary architecture labels again after artifact expected asset names, verify binary architecture labels again after artifact
download, verify source archive contents, generate `checksums.txt`, and verify download, verify source archive contents, generate `checksums.txt`, and verify
@ -217,6 +218,7 @@ Stage 1, implemented now:
- Draft release, manual publish. - Draft release, manual publish.
- Binary architecture validation. - Binary architecture validation.
- Source archive validation. - Source archive validation.
- Local source-archive dry-run coverage.
- SHA-256 checksums for every release asset. - SHA-256 checksums for every release asset.
- Package recipe checksum preflight. - Package recipe checksum preflight.

View file

@ -21,6 +21,15 @@ sha256_of() {
fi fi
} }
require_archive_entry() {
entry=$1
label=$2
printf '%s\n' "$source_listing" |
awk -v target="$entry" '$0 == target { found = 1 } END { exit found ? 0 : 1 }' ||
fail "SOURCE_TARBALL is missing $label"
}
version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h) version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h)
[ -n "$version" ] || fail "could not read TNT_VERSION from include/common.h" [ -n "$version" ] || fail "could not read TNT_VERSION from include/common.h"
release_source="tnt-chat-v${version}-source.tar.gz" release_source="tnt-chat-v${version}-source.tar.gz"
@ -30,14 +39,13 @@ source_tarball=${SOURCE_TARBALL:-${RELEASE_SOURCE_TARBALL:-}}
fail "set SOURCE_TARBALL to the explicit release source archive" fail "set SOURCE_TARBALL to the explicit release source archive"
[ -f "$source_tarball" ] || [ -f "$source_tarball" ] ||
fail "SOURCE_TARBALL does not exist: $source_tarball" fail "SOURCE_TARBALL does not exist: $source_tarball"
tar -tzf "$source_tarball" >/dev/null || source_listing=$(tar -tzf "$source_tarball") ||
fail "SOURCE_TARBALL is not a readable tar.gz archive" fail "SOURCE_TARBALL is not a readable tar.gz archive"
tar -tzf "$source_tarball" | grep -q "^TNT-$version/LICENSE$" || require_archive_entry "TNT-$version/LICENSE" "LICENSE"
fail "SOURCE_TARBALL is missing LICENSE" require_archive_entry "TNT-$version/packaging/README.md" "packaging/README.md"
tar -tzf "$source_tarball" | grep -q "^TNT-$version/packaging/README.md$" || require_archive_entry "TNT-$version/src/tntctl.c" "src/tntctl.c"
fail "SOURCE_TARBALL is missing packaging/README.md" require_archive_entry "TNT-$version/tnt.1" "tnt.1"
tar -tzf "$source_tarball" | grep -q "^TNT-$version/src/tntctl.c$" || require_archive_entry "TNT-$version/tntctl.1" "tntctl.1"
fail "SOURCE_TARBALL is missing src/tntctl.c"
! grep -R "REPLACE_WITH_EMAIL" packaging/arch packaging/debian >/dev/null || ! grep -R "REPLACE_WITH_EMAIL" packaging/arch packaging/debian >/dev/null ||
fail "replace maintainer email placeholders before package publishing" fail "replace maintainer email placeholders before package publishing"

View file

@ -49,6 +49,21 @@ require_file_label() {
fail "unexpected file type for $(basename "$path"): $label" fail "unexpected file type for $(basename "$path"): $label"
} }
archive_has_entry() {
entry=$1
printf '%s\n' "$archive_listing" |
awk -v target="$entry" '$0 == target { found = 1 } END { exit found ? 0 : 1 }'
}
require_archive_entry() {
entry=$1
label=$2
archive_has_entry "$entry" ||
fail "source archive is missing $label"
}
verify_asset() { verify_asset() {
name=$1 name=$1
path=$2 path=$2
@ -69,13 +84,13 @@ verify_asset() {
require_file_label "$path" 'Mach-O 64-bit.*arm64' require_file_label "$path" 'Mach-O 64-bit.*arm64'
;; ;;
tnt-chat-v*-source.tar.gz) tnt-chat-v*-source.tar.gz)
tar -tzf "$path" >/dev/null archive_listing=$(tar -tzf "$path") ||
tar -tzf "$path" | grep -q "^TNT-$VERSION/LICENSE$" || fail "source archive is not a readable tar.gz: $name"
fail "source archive is missing LICENSE" require_archive_entry "TNT-$VERSION/LICENSE" "LICENSE"
tar -tzf "$path" | grep -q "^TNT-$VERSION/src/tntctl.c$" || require_archive_entry "TNT-$VERSION/src/tntctl.c" "src/tntctl.c"
fail "source archive is missing src/tntctl.c" require_archive_entry "TNT-$VERSION/packaging/README.md" "packaging/README.md"
tar -tzf "$path" | grep -q "^TNT-$VERSION/packaging/README.md$" || require_archive_entry "TNT-$VERSION/tnt.1" "tnt.1"
fail "source archive is missing packaging/README.md" require_archive_entry "TNT-$VERSION/tntctl.1" "tntctl.1"
;; ;;
*) *)
fail "unexpected release artifact: $name" fail "unexpected release artifact: $name"

View file

@ -0,0 +1,89 @@
#!/bin/sh
# Build and validate the explicit release source archive.
set -eu
usage() {
cat <<'USAGE'
Usage: scripts/package_source_archive.sh REF [OUT_DIR]
REF is a git ref, commit, or release tag to archive. OUT_DIR defaults to dist.
The archive name is tnt-chat-v$TNT_VERSION-source.tar.gz, and its top-level
directory is TNT-$TNT_VERSION/.
When REF is a SemVer tag such as v1.2.3 or refs/tags/v1.2.3, the tag must match
TNT_VERSION from that ref. This script only builds and validates the archive; it
does not tag, publish, upload, or deploy.
USAGE
}
fail() {
echo "source-archive: $*" >&2
exit 1
}
require_archive_entry() {
entry=$1
label=$2
printf '%s\n' "$archive_listing" |
awk -v target="$entry" '$0 == target { found = 1 } END { exit found ? 0 : 1 }' ||
fail "source archive is missing $label"
}
[ "${1:-}" != "-h" ] && [ "${1:-}" != "--help" ] || {
usage
exit 0
}
REF=${1:-}
OUT_DIR=${2:-dist}
[ -n "$REF" ] || {
usage >&2
exit 2
}
ROOT=$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)
cd "$ROOT"
commit=$(git rev-parse --verify "$REF^{commit}") ||
fail "could not resolve git ref: $REF"
case "$OUT_DIR" in
/*) ;;
*) OUT_DIR="$ROOT/$OUT_DIR" ;;
esac
version=$(git show "$commit:include/common.h" |
sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p')
[ -n "$version" ] || fail "could not read TNT_VERSION from $REF"
case "$REF" in
refs/tags/v[0-9]*.[0-9]*.[0-9]*)
tag=${REF#refs/tags/}
[ "$tag" = "v$version" ] ||
fail "release tag $tag does not match TNT_VERSION $version"
;;
v[0-9]*.[0-9]*.[0-9]*)
[ "$REF" = "v$version" ] ||
fail "release tag $REF does not match TNT_VERSION $version"
;;
esac
archive="$OUT_DIR/tnt-chat-v$version-source.tar.gz"
mkdir -p "$OUT_DIR"
[ ! -e "$archive" ] || fail "output already exists: $archive"
git archive --format=tar.gz --prefix="TNT-$version/" -o "$archive" "$commit"
archive_listing=$(tar -tzf "$archive") ||
fail "archive is not a readable tar.gz: $archive"
require_archive_entry "TNT-$version/LICENSE" "LICENSE"
require_archive_entry "TNT-$version/src/tntctl.c" "src/tntctl.c"
require_archive_entry "TNT-$version/packaging/README.md" "packaging/README.md"
require_archive_entry "TNT-$version/tnt.1" "tnt.1"
require_archive_entry "TNT-$version/tntctl.1" "tntctl.1"
echo "$archive"

View file

@ -198,6 +198,7 @@ sh -n install.sh
sh -n scripts/check_release_ref.sh sh -n scripts/check_release_ref.sh
sh -n scripts/package_publish_check.sh sh -n scripts/package_publish_check.sh
sh -n scripts/package_release_assets.sh sh -n scripts/package_release_assets.sh
sh -n scripts/package_source_archive.sh
scripts/check_release_ref.sh "v$version" scripts/check_release_ref.sh "v$version"
bad_ref=v0.0.0 bad_ref=v0.0.0
[ "$version" != "0.0.0" ] || bad_ref=v9.9.9 [ "$version" != "0.0.0" ] || bad_ref=v9.9.9
@ -266,13 +267,12 @@ if [ "$STRICT" -eq 1 ]; then
fail "replace maintainer email placeholders before strict release" fail "replace maintainer email placeholders before strict release"
step "checking tagged source archive" step "checking tagged source archive"
archive="$tmpdir/tnt-$version-source.tar.gz" archive="$tmpdir/tnt-chat-v$version-source.tar.gz"
archive_extract="$tmpdir/source" archive_extract="$tmpdir/source"
archive_install="$tmpdir/source-install" archive_install="$tmpdir/source-install"
archive_root="$archive_extract/TNT-$version" archive_root="$archive_extract/TNT-$version"
git archive --format=tar.gz --prefix="TNT-$version/" \ scripts/package_source_archive.sh "refs/tags/v$version" "$tmpdir" >/dev/null
-o "$archive" "refs/tags/v$version"
mkdir -p "$archive_extract" mkdir -p "$archive_extract"
tar -xzf "$archive" -C "$archive_extract" tar -xzf "$archive" -C "$archive_extract"

View file

@ -67,12 +67,8 @@ build_artifact_tree() {
write_macho_arm64 "$artifact_root/darwin-arm64/tntctl-darwin-arm64" write_macho_arm64 "$artifact_root/darwin-arm64/tntctl-darwin-arm64"
if [ "$include_source" = "yes" ]; then if [ "$include_source" = "yes" ]; then
source_root="$STATE_DIR/source-$ver/TNT-$ver" mkdir -p "$artifact_root/source"
mkdir -p "$source_root/src" "$source_root/packaging" "$artifact_root/source" ../scripts/package_source_archive.sh HEAD "$artifact_root/source" >/dev/null
printf 'MIT\n' > "$source_root/LICENSE"
printf 'int main(void) { return 0; }\n' > "$source_root/src/tntctl.c"
printf '# Packaging\n' > "$source_root/packaging/README.md"
(cd "$STATE_DIR/source-$ver" && tar -czf "$artifact_root/source/tnt-chat-v$ver-source.tar.gz" "TNT-$ver")
fi fi
} }

103
tests/test_source_archive.sh Executable file
View file

@ -0,0 +1,103 @@
#!/bin/sh
# Release source-archive regression tests.
set -u
PASS=0
FAIL=0
SCRIPT="../scripts/package_source_archive.sh"
STATE_DIR=$(mktemp -d "${TMPDIR:-/tmp}/tnt-source-archive-test.XXXXXX")
cleanup() {
rm -rf "$STATE_DIR"
}
trap cleanup EXIT
pass() {
echo "$1"
PASS=$((PASS + 1))
}
fail() {
echo "$1"
if [ "${2:-}" ]; then
printf '%s\n' "$2"
fi
FAIL=$((FAIL + 1))
}
version() {
sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' ../include/common.h
}
listing_has_entry() {
entry=$1
printf '%s\n' "$ARCHIVE_LISTING" |
awk -v target="$entry" '$0 == target { found = 1 } END { exit found ? 0 : 1 }'
}
echo "=== TNT Source Archive Tests ==="
if [ ! -x "$SCRIPT" ]; then
echo "Error: script $SCRIPT not found or not executable."
exit 1
fi
VER=$(version)
OUT_DIR="$STATE_DIR/out"
OUTPUT=$("$SCRIPT" HEAD "$OUT_DIR" 2>&1)
STATUS=$?
ARCHIVE="$OUT_DIR/tnt-chat-v$VER-source.tar.gz"
if [ "$STATUS" -eq 0 ] &&
[ "$OUTPUT" = "$ARCHIVE" ] &&
[ -s "$ARCHIVE" ]; then
pass "HEAD source archive is built"
else
fail "HEAD source archive build" "$OUTPUT"
fi
ARCHIVE_LISTING=$(tar -tzf "$ARCHIVE" 2>&1)
if listing_has_entry "TNT-$VER/LICENSE" &&
listing_has_entry "TNT-$VER/src/tntctl.c" &&
listing_has_entry "TNT-$VER/packaging/README.md" &&
listing_has_entry "TNT-$VER/tnt.1" &&
listing_has_entry "TNT-$VER/tntctl.1"; then
pass "source archive contains required release files"
else
fail "source archive required files" "$(printf '%s\n' "$ARCHIVE_LISTING" | sed -n '1,40p')"
fi
DUP_OUTPUT=$("$SCRIPT" HEAD "$OUT_DIR" 2>&1)
DUP_STATUS=$?
if [ "$DUP_STATUS" -ne 0 ] &&
printf '%s\n' "$DUP_OUTPUT" | grep -q 'output already exists'; then
pass "existing archive is not overwritten"
else
fail "existing archive handling" "$DUP_OUTPUT"
fi
BAD_OUTPUT=$("$SCRIPT" refs/heads/does-not-exist "$STATE_DIR/bad" 2>&1)
BAD_STATUS=$?
if [ "$BAD_STATUS" -ne 0 ] &&
printf '%s\n' "$BAD_OUTPUT" | grep -q 'could not resolve git ref'; then
pass "missing git ref is rejected"
else
fail "missing git ref handling" "$BAD_OUTPUT"
fi
HELP_OUTPUT=$("$SCRIPT" --help 2>&1)
HELP_STATUS=$?
if [ "$HELP_STATUS" -eq 0 ] &&
printf '%s\n' "$HELP_OUTPUT" | grep -q 'Usage: scripts/package_source_archive.sh REF'; then
pass "help output is available"
else
fail "help output handling" "$HELP_OUTPUT"
fi
echo ""
echo "PASSED: $PASS"
echo "FAILED: $FAIL"
[ "$FAIL" -eq 0 ] && echo "All tests passed" || echo "Some tests failed"
exit "$FAIL"