From a51d0fb6053b944810d8c50d912687a35a081d40 Mon Sep 17 00:00:00 2001 From: m1ngsama Date: Thu, 28 May 2026 15:19:20 +0800 Subject: [PATCH] Extract release source archive packaging --- .github/workflows/release.yml | 12 +--- Makefile | 1 + docs/CHANGELOG.md | 3 + docs/CICD.md | 4 +- scripts/package_publish_check.sh | 22 ++++-- scripts/package_release_assets.sh | 29 ++++++-- scripts/package_source_archive.sh | 89 ++++++++++++++++++++++++ scripts/release_check.sh | 6 +- tests/test_release_artifact_gate.sh | 8 +-- tests/test_source_archive.sh | 103 ++++++++++++++++++++++++++++ 10 files changed, 243 insertions(+), 34 deletions(-) create mode 100755 scripts/package_source_archive.sh create mode 100755 tests/test_source_archive.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f5d273f..423cca1 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -109,16 +109,8 @@ jobs: - name: Build source archive run: | - set -eu - version=$(sed -n 's/^#define TNT_VERSION "\([^"]*\)".*/\1/p' include/common.h) - 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" + archive=$(scripts/package_source_archive.sh "${GITHUB_REF_NAME}" dist) + sha256sum "$archive" - name: Upload source archive uses: actions/upload-artifact@v4 diff --git a/Makefile b/Makefile index b948105..03fe85c 100644 --- a/Makefile +++ b/Makefile @@ -134,6 +134,7 @@ script-test: all @cd tests && ./test_docs_help_surface.sh @cd tests && ./test_logrotate.sh @cd tests && ./test_message_log_tool.sh + @cd tests && ./test_source_archive.sh @cd tests && ./test_release_artifact_gate.sh integration-test: all diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 7eca2ff..396c21f 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -12,6 +12,9 @@ - Added CI governance layers for fast PR checks, release-branch validation, extended runtime validation, container portability builds, and package recipe 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 values, env keys, and accepted numeric ranges. - Added a dedicated `tntctl_text` module with unit coverage for local diff --git a/docs/CICD.md b/docs/CICD.md index aa1649d..2703381 100644 --- a/docs/CICD.md +++ b/docs/CICD.md @@ -67,7 +67,8 @@ Runs only for SemVer tags matching `vMAJOR.MINOR.PATCH`: - Builds Linux glibc AMD64 and ARM64 binaries. - Builds macOS Intel and Apple Silicon binaries. - 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 expected asset names, verify binary architecture labels again after artifact download, verify source archive contents, generate `checksums.txt`, and verify @@ -217,6 +218,7 @@ Stage 1, implemented now: - Draft release, manual publish. - Binary architecture validation. - Source archive validation. +- Local source-archive dry-run coverage. - SHA-256 checksums for every release asset. - Package recipe checksum preflight. diff --git a/scripts/package_publish_check.sh b/scripts/package_publish_check.sh index da9a0cc..5b2c0f7 100755 --- a/scripts/package_publish_check.sh +++ b/scripts/package_publish_check.sh @@ -21,6 +21,15 @@ sha256_of() { 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) [ -n "$version" ] || fail "could not read TNT_VERSION from include/common.h" 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" [ -f "$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" -tar -tzf "$source_tarball" | grep -q "^TNT-$version/LICENSE$" || - fail "SOURCE_TARBALL is missing LICENSE" -tar -tzf "$source_tarball" | grep -q "^TNT-$version/packaging/README.md$" || - fail "SOURCE_TARBALL is missing packaging/README.md" -tar -tzf "$source_tarball" | grep -q "^TNT-$version/src/tntctl.c$" || - fail "SOURCE_TARBALL is missing src/tntctl.c" +require_archive_entry "TNT-$version/LICENSE" "LICENSE" +require_archive_entry "TNT-$version/packaging/README.md" "packaging/README.md" +require_archive_entry "TNT-$version/src/tntctl.c" "src/tntctl.c" +require_archive_entry "TNT-$version/tnt.1" "tnt.1" +require_archive_entry "TNT-$version/tntctl.1" "tntctl.1" ! grep -R "REPLACE_WITH_EMAIL" packaging/arch packaging/debian >/dev/null || fail "replace maintainer email placeholders before package publishing" diff --git a/scripts/package_release_assets.sh b/scripts/package_release_assets.sh index 26c0753..a88dfa1 100755 --- a/scripts/package_release_assets.sh +++ b/scripts/package_release_assets.sh @@ -49,6 +49,21 @@ require_file_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() { name=$1 path=$2 @@ -69,13 +84,13 @@ verify_asset() { require_file_label "$path" 'Mach-O 64-bit.*arm64' ;; tnt-chat-v*-source.tar.gz) - tar -tzf "$path" >/dev/null - tar -tzf "$path" | grep -q "^TNT-$VERSION/LICENSE$" || - fail "source archive is missing LICENSE" - tar -tzf "$path" | grep -q "^TNT-$VERSION/src/tntctl.c$" || - fail "source archive is missing src/tntctl.c" - tar -tzf "$path" | grep -q "^TNT-$VERSION/packaging/README.md$" || - fail "source archive is missing packaging/README.md" + archive_listing=$(tar -tzf "$path") || + fail "source archive is not a readable tar.gz: $name" + 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" ;; *) fail "unexpected release artifact: $name" diff --git a/scripts/package_source_archive.sh b/scripts/package_source_archive.sh new file mode 100755 index 0000000..5e5f8f5 --- /dev/null +++ b/scripts/package_source_archive.sh @@ -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" diff --git a/scripts/release_check.sh b/scripts/release_check.sh index b255fb8..18a1382 100755 --- a/scripts/release_check.sh +++ b/scripts/release_check.sh @@ -198,6 +198,7 @@ sh -n install.sh sh -n scripts/check_release_ref.sh sh -n scripts/package_publish_check.sh sh -n scripts/package_release_assets.sh +sh -n scripts/package_source_archive.sh scripts/check_release_ref.sh "v$version" bad_ref=v0.0.0 [ "$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" 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_install="$tmpdir/source-install" archive_root="$archive_extract/TNT-$version" - git archive --format=tar.gz --prefix="TNT-$version/" \ - -o "$archive" "refs/tags/v$version" + scripts/package_source_archive.sh "refs/tags/v$version" "$tmpdir" >/dev/null mkdir -p "$archive_extract" tar -xzf "$archive" -C "$archive_extract" diff --git a/tests/test_release_artifact_gate.sh b/tests/test_release_artifact_gate.sh index ea3a496..b28cbdb 100755 --- a/tests/test_release_artifact_gate.sh +++ b/tests/test_release_artifact_gate.sh @@ -67,12 +67,8 @@ build_artifact_tree() { write_macho_arm64 "$artifact_root/darwin-arm64/tntctl-darwin-arm64" if [ "$include_source" = "yes" ]; then - source_root="$STATE_DIR/source-$ver/TNT-$ver" - mkdir -p "$source_root/src" "$source_root/packaging" "$artifact_root/source" - 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") + mkdir -p "$artifact_root/source" + ../scripts/package_source_archive.sh HEAD "$artifact_root/source" >/dev/null fi } diff --git a/tests/test_source_archive.sh b/tests/test_source_archive.sh new file mode 100755 index 0000000..36ff781 --- /dev/null +++ b/tests/test_source_archive.sh @@ -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"