name: Prepare draft release on: workflow_dispatch: inputs: tag: description: Existing release tag default: v0.4.2 required: true type: string commit_sha: description: Exact approved master commit SHA required: true type: string permissions: contents: read jobs: verify: name: Verify release ref runs-on: ubuntu-24.04 timeout-minutes: 10 steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false ref: ${{ inputs.commit_sha }} - name: Require the approved v0.4.2 tag and master commit env: EXPECTED_SHA: ${{ inputs.commit_sha }} RELEASE_TAG: ${{ inputs.tag }} shell: bash run: | set -euo pipefail if [[ ! "$EXPECTED_SHA" =~ ^[0-9a-f]{40}$ ]]; then echo "commit_sha must be a full lowercase commit SHA" >&2 exit 1 fi if [[ "$RELEASE_TAG" != "v0.4.2" ]]; then echo "this workflow prepares only v0.4.2" >&2 exit 1 fi if ! git show-ref --verify --quiet "refs/tags/$RELEASE_TAG"; then echo "tag $RELEASE_TAG does not exist" >&2 exit 1 fi head_sha=$(git rev-parse HEAD) tag_sha=$(git rev-parse "refs/tags/$RELEASE_TAG^{commit}") master_sha=$(git rev-parse "refs/remotes/origin/master^{commit}") test "$head_sha" = "$EXPECTED_SHA" test "$tag_sha" = "$EXPECTED_SHA" test "$master_sha" = "$EXPECTED_SHA" test -z "$(git status --porcelain --untracked-files=all)" candidates: name: ${{ matrix.goos }}/${{ matrix.goarch }} needs: verify strategy: fail-fast: false matrix: include: - goos: linux goarch: amd64 runner: ubuntu-24.04 runner_os: Linux runner_arch: X64 herdr_asset: herdr-linux-x86_64 herdr_sha256: 2a02fed16beb651ef006e1d43f048f652ca4dc58ad053cd2d44450563d5c54b7 - goos: linux goarch: arm64 runner: ubuntu-24.04-arm runner_os: Linux runner_arch: ARM64 herdr_asset: herdr-linux-aarch64 herdr_sha256: f4ccf4de745f2cb9a39a983e9ba3703dad50ec2a58dea83026ceab721bbd8d9e - goos: darwin goarch: amd64 runner: macos-15-intel runner_os: macOS runner_arch: X64 herdr_asset: herdr-macos-x86_64 herdr_sha256: 053be0639935fe54ab5efbdb46651054e4f6a753a5b43153c88bd6912bce1e94 - goos: darwin goarch: arm64 runner: macos-15 runner_os: macOS runner_arch: ARM64 herdr_asset: herdr-macos-aarch64 herdr_sha256: 5fc7a7e7adfaca56fa80aa89dcb025693357268dab8285b9ce2d08a2313c89de runs-on: ${{ matrix.runner }} timeout-minutes: 45 env: CGO_ENABLED: "0" GOMAXPROCS: "2" GOFLAGS: "-p=2" NODE_OPTIONS: "--max-old-space-size=512" steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: fetch-depth: 0 persist-credentials: false ref: ${{ inputs.commit_sha }} - uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 with: go-version-file: go.mod cache-dependency-path: go.sum - uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version: "20" cache: npm cache-dependency-path: web/package-lock.json - name: Prove the native runner env: EXPECTED_ARCH: ${{ matrix.runner_arch }} EXPECTED_GOARCH: ${{ matrix.goarch }} EXPECTED_GOOS: ${{ matrix.goos }} EXPECTED_OS: ${{ matrix.runner_os }} shell: bash run: | set -euo pipefail test "$RUNNER_OS" = "$EXPECTED_OS" test "$RUNNER_ARCH" = "$EXPECTED_ARCH" test "$(go env GOOS)" = "$EXPECTED_GOOS" test "$(go env GOARCH)" = "$EXPECTED_GOARCH" test "$(git rev-parse HEAD)" = "${{ inputs.commit_sha }}" test -z "$(git status --porcelain --untracked-files=all)" - name: Test and verify embedded browser assets shell: bash run: | set -euo pipefail npm ci --prefix web npm test --prefix web npm run typecheck --prefix web npm run build --prefix web git diff --exit-code -- web/dist test -z "$(git status --short --untracked-files=all -- web/dist)" - name: Test, build, package, and verify env: RELEASE_TAG: ${{ inputs.tag }} TARGET_GOARCH: ${{ matrix.goarch }} TARGET_GOOS: ${{ matrix.goos }} shell: bash run: | set -euo pipefail go test ./... archive_base="shepherdr_0.4.2_${TARGET_GOOS}_${TARGET_GOARCH}" candidate_dir="$RUNNER_TEMP/candidate" package_dir="$candidate_dir/$archive_base" mkdir -p "$package_dir" go build -trimpath -ldflags "-s -w -X main.releaseVersion=$RELEASE_TAG" -o "$package_dir/shepherdr" . test "$("$package_dir/shepherdr" --version)" = "shepherdr v0.4.2" go version -m "$package_dir/shepherdr" > "$candidate_dir/build-metadata.txt" grep -F "github.com/luiscleto/shepherdr" "$candidate_dir/build-metadata.txt" grep -F "CGO_ENABLED=0" "$candidate_dir/build-metadata.txt" grep -F "GOOS=$TARGET_GOOS" "$candidate_dir/build-metadata.txt" grep -F "GOARCH=$TARGET_GOARCH" "$candidate_dir/build-metadata.txt" grep -F "vcs.revision=${{ inputs.commit_sha }}" "$candidate_dir/build-metadata.txt" grep -F "vcs.modified=false" "$candidate_dir/build-metadata.txt" cp README.md CHANGELOG.md LICENSE "$package_dir/" COPYFILE_DISABLE=1 tar -czf "$candidate_dir/$archive_base.tar.gz" -C "$candidate_dir" "$archive_base" cd "$candidate_dir" shasum -a 256 "$archive_base.tar.gz" > "$archive_base.tar.gz.sha256" shasum -a 256 -c "$archive_base.tar.gz.sha256" extract_root="$candidate_dir/extracted" mkdir "$extract_root" tar -xzf "$archive_base.tar.gz" -C "$extract_root" test "$(find "$extract_root" -mindepth 1 -maxdepth 1 -type d | wc -l | tr -d ' ')" = "1" test "$(find "$extract_root" -mindepth 1 | wc -l | tr -d ' ')" = "5" test "$(find "$extract_root" -type f | wc -l | tr -d ' ')" = "4" test "$(find "$extract_root" -type l | wc -l | tr -d ' ')" = "0" test -x "$extract_root/$archive_base/shepherdr" test -f "$extract_root/$archive_base/README.md" test -f "$extract_root/$archive_base/CHANGELOG.md" test -f "$extract_root/$archive_base/LICENSE" test "$("$extract_root/$archive_base/shepherdr" --version)" = "shepherdr v0.4.2" - name: Exercise the native packaged binary with Herdr 0.9.1 env: GH_TOKEN: ${{ github.token }} HERDR_ASSET: ${{ matrix.herdr_asset }} HERDR_SHA256: ${{ matrix.herdr_sha256 }} TARGET_GOARCH: ${{ matrix.goarch }} TARGET_GOOS: ${{ matrix.goos }} shell: bash run: | set -euo pipefail archive_base="shepherdr_0.4.2_${TARGET_GOOS}_${TARGET_GOARCH}" packaged_binary="$RUNNER_TEMP/candidate/extracted/$archive_base/shepherdr" smoke_root="$RUNNER_TEMP/native-smoke" tools_dir="$smoke_root/tools" mkdir -p "$tools_dir" gh release download v0.9.1 --repo herdrdev/herdr --pattern "$HERDR_ASSET" --dir "$tools_dir" downloaded_herdr="$tools_dir/$HERDR_ASSET" printf '%s %s\n' "$HERDR_SHA256" "$downloaded_herdr" | shasum -a 256 -c - install -m 0755 "$downloaded_herdr" "$tools_dir/herdr" herdr_binary="$tools_dir/herdr" test "$("$herdr_binary" --version)" = "herdr 0.9.1" test "$(PATH="$tools_dir:$PATH" command -v herdr)" = "$herdr_binary" herdr_pid= shepherdr_pid= wait_bounded() { local pid=$1 local label=$2 local seconds=$3 local allow_failure=$4 local attempt status for ((attempt = 0; attempt < seconds; attempt++)); do if ! kill -0 "$pid" 2>/dev/null; then if wait "$pid"; then return 0 else status=$? fi if [[ "$allow_failure" == "true" ]]; then return 0 fi echo "$label exited with status $status" >&2 return "$status" fi sleep 1 done echo "$label did not exit within ${seconds}s" >&2 return 124 } terminate_bounded() { local pid=$1 local label=$2 if ! kill -0 "$pid" 2>/dev/null; then wait "$pid" >/dev/null 2>&1 || true return 0 fi kill -TERM "$pid" 2>/dev/null || true if wait_bounded "$pid" "$label" 10 true; then return 0 fi echo "$label ignored TERM; sending KILL" >&2 kill -KILL "$pid" 2>/dev/null || true wait_bounded "$pid" "$label after KILL" 5 true || echo "$label could not be reaped" >&2 return 1 } stop_herdr_bounded() { local stop_pid HERDR_CONFIG_PATH="$smoke_root/herdr/config.toml" HERDR_SOCKET_PATH="$smoke_root/herdr/herdr.sock" "$herdr_binary" server stop >"$smoke_root/herdr-stop.log" 2>&1 & stop_pid=$! if ! wait_bounded "$stop_pid" "herdr stop command" 10 false; then terminate_bounded "$stop_pid" "herdr stop command cleanup" || true return 1 fi wait_bounded "$herdr_pid" "Herdr server" 15 false } cleanup() { status=$? trap - EXIT cleanup_failed=0 if [[ -n "$shepherdr_pid" ]]; then if kill -0 "$shepherdr_pid" 2>/dev/null; then if ! terminate_bounded "$shepherdr_pid" "Shepherdr cleanup"; then cleanup_failed=1 fi else wait "$shepherdr_pid" >/dev/null 2>&1 || true fi fi if [[ -n "$herdr_pid" ]]; then if kill -0 "$herdr_pid" 2>/dev/null; then HERDR_CONFIG_PATH="$smoke_root/herdr/config.toml" HERDR_SOCKET_PATH="$smoke_root/herdr/herdr.sock" "$herdr_binary" server stop >"$smoke_root/herdr-cleanup-stop.log" 2>&1 & cleanup_stop_pid=$! if ! wait_bounded "$cleanup_stop_pid" "herdr cleanup stop command" 10 false; then cleanup_failed=1 terminate_bounded "$cleanup_stop_pid" "herdr cleanup stop command" || true fi if ! wait_bounded "$herdr_pid" "Herdr cleanup" 10 true; then cleanup_failed=1 terminate_bounded "$herdr_pid" "Herdr cleanup" || true fi else wait "$herdr_pid" >/dev/null 2>&1 || true fi fi if [[ "$status" -eq 0 && "$cleanup_failed" -ne 0 ]]; then status=1 fi exit "$status" } trap cleanup EXIT mkdir -p "$smoke_root/herdr" HERDR_CONFIG_PATH="$smoke_root/herdr/config.toml" HERDR_SOCKET_PATH="$smoke_root/herdr/herdr.sock" "$herdr_binary" server >"$smoke_root/herdr.log" 2>&1 & herdr_pid=$! for _ in $(seq 1 60); do if [[ -S "$smoke_root/herdr/herdr.sock" ]]; then break fi sleep 1 done test -S "$smoke_root/herdr/herdr.sock" test "$(ps -o uid= -p "$herdr_pid" | tr -d ' ')" = "$(id -u)" HERDR_CONFIG_PATH="$smoke_root/herdr/config.toml" HERDR_SOCKET_PATH="$smoke_root/herdr/herdr.sock" "$herdr_binary" status server --json >"$smoke_root/herdr-status.json" grep -F '"running":true' "$smoke_root/herdr-status.json" grep -F '"version":"0.9.1"' "$smoke_root/herdr-status.json" mkdir -p "$smoke_root/sign-in-off/config" "$smoke_root/sign-in-off/uploads" XDG_CONFIG_HOME="$smoke_root/sign-in-off/config" PATH="$tools_dir:$PATH" "$packaged_binary" -no-sign-in -listen 127.0.0.1:18787 -herdr-socket "$smoke_root/herdr/herdr.sock" -upload-parent "$smoke_root/sign-in-off/uploads" >"$smoke_root/sign-in-off.log" 2>&1 & shepherdr_pid=$! for _ in $(seq 1 60); do if curl -fsS http://127.0.0.1:18787/healthz >"$smoke_root/health.txt"; then break fi sleep 1 done test "$(cat "$smoke_root/health.txt")" = "ok" test "$(ps -o uid= -p "$shepherdr_pid" | tr -d ' ')" = "$(id -u)" curl -fsS http://127.0.0.1:18787/ >"$smoke_root/home.html" grep -F '