| 1 | #!/usr/bin/env bash |
| 2 | set -euo pipefail |
| 3 | |
| 4 | if [[ $# -ne 2 ]]; then |
| 5 | echo "usage: $0 INPUT_ARTIFACT_DIR OUTPUT_BUNDLE_DIR" >&2 |
| 6 | exit 2 |
| 7 | fi |
| 8 | |
| 9 | artifact_dir="$1" |
| 10 | bundle_dir="$2" |
| 11 | |
| 12 | # Archive metadata must be stable across recovery builds. The public workflow |
| 13 | # still refuses to replace existing release assets; reproducible packaging is a |
| 14 | # diagnostic and provenance aid, not permission to overwrite published bytes. |
| 15 | export TZ=UTC |
| 16 | if [[ -z "${SOURCE_DATE_EPOCH:-}" ]]; then |
| 17 | echo "SOURCE_DATE_EPOCH is required; set it to the tagged/source commit timestamp" >&2 |
| 18 | exit 1 |
| 19 | fi |
| 20 | if ! [[ "${SOURCE_DATE_EPOCH}" =~ ^[0-9]+$ ]]; then |
| 21 | echo "SOURCE_DATE_EPOCH must be an integer Unix timestamp, got: ${SOURCE_DATE_EPOCH}" >&2 |
| 22 | exit 1 |
| 23 | fi |
| 24 | |
| 25 | # Trim leading zeroes before the length and range checks, avoiding arithmetic |
| 26 | # overflow from malformed values. ZIP stores DOS timestamps, whose valid range |
| 27 | # is 1980-01-01T00:00:00Z through 2107-12-31T23:59:58Z. |
| 28 | source_date_epoch="${SOURCE_DATE_EPOCH#"${SOURCE_DATE_EPOCH%%[!0]*}"}" |
| 29 | source_date_epoch="${source_date_epoch:-0}" |
| 30 | if (( ${#source_date_epoch} > 10 )) || |
| 31 | (( 10#${source_date_epoch} < 315532800 || 10#${source_date_epoch} > 4354819198 )); then |
| 32 | echo "SOURCE_DATE_EPOCH must be between 315532800 (1980-01-01T00:00:00Z) and 4354819198 (2107-12-31T23:59:58Z) for ZIP archives" >&2 |
| 33 | exit 1 |
| 34 | fi |
| 35 | source_date_epoch="$((10#${source_date_epoch}))" |
| 36 | |
| 37 | if date --version >/dev/null 2>&1; then |
| 38 | archive_timestamp="$(date -u -d "@${source_date_epoch}" '+%Y%m%d%H%M.%S')" |
| 39 | else |
| 40 | archive_timestamp="$(date -u -r "${source_date_epoch}" '+%Y%m%d%H%M.%S')" |
| 41 | fi |
| 42 | |
| 43 | if [[ ! -d "${artifact_dir}" ]]; then |
| 44 | echo "input artifact directory does not exist: ${artifact_dir}" >&2 |
| 45 | exit 1 |
| 46 | fi |
| 47 | artifact_dir="$(cd "${artifact_dir}" && pwd)" |
| 48 | |
| 49 | if [[ -e "${bundle_dir}" && ! -d "${bundle_dir}" ]]; then |
| 50 | echo "output bundle path is not a directory: ${bundle_dir}" >&2 |
| 51 | exit 1 |
| 52 | fi |
| 53 | if [[ -e "${bundle_dir}" && -n "$(find "${bundle_dir}" -mindepth 1 -maxdepth 1 -print -quit)" ]]; then |
| 54 | echo "output bundle directory must be empty: ${bundle_dir}" >&2 |
| 55 | exit 1 |
| 56 | fi |
| 57 | mkdir -p "${bundle_dir}" |
| 58 | bundle_dir="$(cd "${bundle_dir}" && pwd)" |
| 59 | |
| 60 | manifest="${bundle_dir}/codewhale-bundles-sha256.txt" |
| 61 | : > "${manifest}" |
| 62 | |
| 63 | # Windows archives must contain CRLF batch files regardless of the builder's |
| 64 | # working-tree line endings (`* text=auto` checks out LF on macOS/Linux). |
| 65 | write_crlf_file() { |
| 66 | local src="$1" |
| 67 | local dest="$2" |
| 68 | if [[ ! -f "${src}" ]]; then |
| 69 | echo "missing Windows launcher or install script: ${src}" >&2 |
| 70 | exit 1 |
| 71 | fi |
| 72 | awk '{ sub(/\r$/, ""); printf "%s\r\n", $0 }' "${src}" > "${dest}" |
| 73 | } |
| 74 | |
| 75 | bundle() { |
| 76 | local platform="$1" |
| 77 | local cli_src="$2" |
| 78 | local shim_src="$3" |
| 79 | local ext="$4" |
| 80 | local variant="$5" |
| 81 | |
| 82 | local stem="codewhale-${platform}${variant:+-}${variant}" |
| 83 | local cli_dst="codewhale" |
| 84 | local shim_dst="codew" |
| 85 | if [[ "${platform}" == windows-* ]]; then |
| 86 | cli_dst="codewhale.exe" |
| 87 | shim_dst="codew.exe" |
| 88 | fi |
| 89 | |
| 90 | local cli_path="${artifact_dir}/${cli_src}/${cli_src}" |
| 91 | local shim_path="${artifact_dir}/${shim_src}/${shim_src}" |
| 92 | if [[ ! -f "${cli_path}" ]]; then |
| 93 | echo "missing required release artifact for ${platform}: ${cli_path}" >&2 |
| 94 | exit 1 |
| 95 | fi |
| 96 | if [[ ! -f "${shim_path}" ]]; then |
| 97 | echo "missing required release artifact for ${platform}: ${shim_path}" >&2 |
| 98 | exit 1 |
| 99 | fi |
| 100 | |
| 101 | local stage_root |
| 102 | stage_root="$(mktemp -d)" |
| 103 | local stage_dir="${stage_root}/${stem}" |
| 104 | mkdir -p "${stage_dir}" |
| 105 | |
| 106 | cp "${cli_path}" "${stage_dir}/${cli_dst}" |
| 107 | cp "${shim_path}" "${stage_dir}/${shim_dst}" |
| 108 | |
| 109 | # actions/upload-artifact intentionally normalizes downloaded files to 0644. |
| 110 | # Restore the executable contract before constructing Unix archives. |
| 111 | if [[ "${platform}" != windows-* ]]; then |
| 112 | chmod 0755 \ |
| 113 | "${stage_dir}/${cli_dst}" \ |
| 114 | "${stage_dir}/${shim_dst}" |
| 115 | fi |
| 116 | |
| 117 | # Regular and portable Windows zips ship the Terminal-aware launcher (#1854). |
| 118 | # The GitHub flat asset `codewhale.bat` still targets the x64 release filename; |
| 119 | # archives rename the binary to codewhale.exe, so they reuse the NSIS launcher. |
| 120 | if [[ "${platform}" == windows-* ]]; then |
| 121 | write_crlf_file \ |
| 122 | scripts/installer/codewhale.bat \ |
| 123 | "${stage_dir}/codewhale.bat" |
| 124 | fi |
| 125 | |
| 126 | if [[ "${variant}" != "portable" ]]; then |
| 127 | if [[ "${platform}" == windows-* ]]; then |
| 128 | write_crlf_file \ |
| 129 | scripts/release/install.bat \ |
| 130 | "${stage_dir}/install.bat" |
| 131 | else |
| 132 | cp scripts/release/install.sh "${stage_dir}/" |
| 133 | chmod +x "${stage_dir}/install.sh" |
| 134 | fi |
| 135 | fi |
| 136 | |
| 137 | # zip and tar both record mtimes; normalize every staged entry to the exact |
| 138 | # source commit timestamp so identical inputs do not produce checksum drift. |
| 139 | find "${stage_dir}" -exec touch -t "${archive_timestamp}" {} + |
| 140 | |
| 141 | local archive="${bundle_dir}/${stem}.${ext}" |
| 142 | if [[ "${ext}" == "zip" ]]; then |
| 143 | (cd "${stage_root}" && zip -Xqr "${archive}" "${stem}/") |
| 144 | elif tar --version 2>/dev/null | grep -q 'GNU tar'; then |
| 145 | tar \ |
| 146 | --sort=name \ |
| 147 | --mtime="@${source_date_epoch}" \ |
| 148 | --owner=0 \ |
| 149 | --group=0 \ |
| 150 | --numeric-owner \ |
| 151 | --format=ustar \ |
| 152 | -cf - \ |
| 153 | -C "${stage_root}" \ |
| 154 | "${stem}/" | gzip -n > "${archive}" |
| 155 | else |
| 156 | COPYFILE_DISABLE=1 tar -cf - -C "${stage_root}" "${stem}/" | gzip -n > "${archive}" |
| 157 | fi |
| 158 | |
| 159 | local checksum |
| 160 | checksum="$(sha256sum "${archive}" | awk '{print $1}')" |
| 161 | printf '%s %s\n' "${checksum}" "$(basename "${archive}")" >> "${manifest}" |
| 162 | rm -rf "${stage_root}" |
| 163 | echo "Created ${archive}" |
| 164 | } |
| 165 | |
| 166 | bundle linux-x64 \ |
| 167 | codewhale-linux-x64 codew-linux-x64 tar.gz "" |
| 168 | bundle linux-arm64 \ |
| 169 | codewhale-linux-arm64 codew-linux-arm64 tar.gz "" |
| 170 | bundle android-arm64 \ |
| 171 | codewhale-android-arm64 codew-android-arm64 tar.gz "" |
| 172 | bundle macos-x64 \ |
| 173 | codewhale-macos-x64 codew-macos-x64 tar.gz "" |
| 174 | bundle macos-arm64 \ |
| 175 | codewhale-macos-arm64 codew-macos-arm64 tar.gz "" |
| 176 | bundle windows-x64 \ |
| 177 | codewhale-windows-x64.exe codew-windows-x64.exe zip "" |
| 178 | bundle windows-x64 \ |
| 179 | codewhale-windows-x64.exe codew-windows-x64.exe zip portable |
| 180 | bundle windows-arm64 \ |
| 181 | codewhale-windows-arm64.exe codew-windows-arm64.exe zip "" |
| 182 | bundle windows-arm64 \ |
| 183 | codewhale-windows-arm64.exe codew-windows-arm64.exe zip portable |
| 184 | |
| 185 | sort -o "${manifest}" "${manifest}" |
| 186 | echo "Bundle checksum manifest:" |
| 187 | cat "${manifest}" |
| 188 |