From 93b250b15527103afbb34f6e3f1cec64f94337ad Mon Sep 17 00:00:00 2001 From: Jesse Vincent Date: Thu, 16 Jul 2026 03:32:25 +0000 Subject: [PATCH] fix(codex): make package script and its test portable beyond macOS/bsdtar The packaging pipeline only worked on a Mac with default umask, for three stacked reasons: - The deterministic-metadata tar flags (--uid/--gid/--uname/--gname) are bsdtar spellings; GNU tar rejects them, so the tar.gz archive step died on Linux. Detect the tar flavor and use --owner=:0 --group=:0 --numeric-owner on GNU tar, which writes byte-identical ustar headers (uid/gid 0, empty uname/gname). - Staged file modes depended on two umasks canceling out: git archive masks entry modes with tar.umask (git default 0002 -> 775), and the unflagged tar extraction re-masked with the process umask (022 on macOS -> 755, but 002 elsewhere -> 775). Pin tar.umask=0022 on the archive call and extract with -p so staged modes are canonical 755/644 on every machine. - The test's timestamp assertion parsed bsdtar's -tv column layout and expected epoch 0 rendered in a US timezone ("Dec 31 1969"); GNU tar uses different columns and UTC hosts render "1970-01-01". Assert mtime == 0 via python3 tarfile instead, matching how the test already checks zip timestamps. tests/codex/test-package-codex-plugin.sh now passes on Linux/GNU tar; the bsdtar branch preserves the exact flags that passed on macOS. --- scripts/package-codex-plugin.sh | 17 +++++++++++++---- tests/codex/test-package-codex-plugin.sh | 9 +++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/scripts/package-codex-plugin.sh b/scripts/package-codex-plugin.sh index 00399f06..5308e28d 100755 --- a/scripts/package-codex-plugin.sh +++ b/scripts/package-codex-plugin.sh @@ -230,14 +230,16 @@ prepare_metadata_root() { METADATA_ROOT="$(prepare_metadata_root "$METADATA_SOURCE")" -git -C "$REPO_ROOT" archive --format=tar "$REF" -- \ +# Pin tar.umask and extract with -p so staged modes are canonical 755/644 +# regardless of the builder's git config or process umask. +git -C "$REPO_ROOT" -c tar.umask=0022 archive --format=tar "$REF" -- \ .codex-plugin \ CODE_OF_CONDUCT.md \ LICENSE \ README.md \ assets \ skills \ - | tar -xf - -C "$STAGE" + | tar -xpf - -C "$STAGE" VERSION="$(jq -r '.version // empty' "$STAGE/.codex-plugin/plugin.json")" [[ -n "$VERSION" ]] || die "could not read version from .codex-plugin/plugin.json" @@ -298,12 +300,19 @@ case "$FORMAT" in ) ;; tar.gz) - # Match the prior official archive's deterministic tar entry metadata. + # Match the prior official archive's deterministic tar entry metadata: + # ustar entries with uid/gid 0 and empty uname/gname. GNU tar and bsdtar + # (macOS) spell those flags differently. + if tar --version 2>/dev/null | grep -q 'GNU tar'; then + TAR_METADATA_FLAGS=(--owner=:0 --group=:0 --numeric-owner) + else + TAR_METADATA_FLAGS=(--uid 0 --gid 0 --uname '' --gname '') + fi TZ=UTC find "$STAGE" -exec touch -t 197001010000 {} + ( cd "$STAGE" rm -f "$OUTPUT" - COPYFILE_DISABLE=1 tar -cf - --no-recursion --format ustar --uid 0 --gid 0 --uname '' --gname '' -T "$ARCHIVE_LIST" | + COPYFILE_DISABLE=1 tar -cf - --no-recursion --format ustar "${TAR_METADATA_FLAGS[@]}" -T "$ARCHIVE_LIST" | gzip -9n >"$OUTPUT" ) ;; diff --git a/tests/codex/test-package-codex-plugin.sh b/tests/codex/test-package-codex-plugin.sh index 62c73f1c..947cb6db 100755 --- a/tests/codex/test-package-codex-plugin.sh +++ b/tests/codex/test-package-codex-plugin.sh @@ -210,8 +210,13 @@ assert_equals "$tar_archive_paths" "$archive_paths" "zip and tar.gz archives con tar_task_brief_mode="$(tar -tzvf "$tar_archive" skills/subagent-driven-development/scripts/task-brief | awk '{print $1}')" assert_equals "$tar_task_brief_mode" "-rwxr-xr-x" "tar.gz archive preserves executable script mode" -tar_metadata_times="$(tar -tzvf "$tar_archive" | awk '{print $6, $7, $8}' | sort -u)" -assert_equals "$tar_metadata_times" "Dec 31 1969" "tar.gz archive normalizes entry timestamps" +tar_metadata_times="$(python3 - "$tar_archive" <<'PY' +import sys, tarfile +with tarfile.open(sys.argv[1]) as archive: + print(sorted({member.mtime for member in archive.getmembers()})) +PY +)" +assert_equals "$tar_metadata_times" "[0]" "tar.gz archive normalizes entry timestamps" metadata_archive="$TEST_ROOT/metadata-source.tar.gz" metadata_zip="$TEST_ROOT/metadata-source.zip"