Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/publish-tabex-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: publish-tabex-release

on:
workflow_dispatch:
inputs:
release_tag:
description: Exact stable private Tabex release tag to mirror (for example v0.0.12)
required: true
type: string

permissions:
actions: write
contents: write

concurrency:
group: publish-tabex-release
cancel-in-progress: false

jobs:
publish:
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
steps:
- name: Checkout trusted publisher
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: main

- name: Publish verified public binary assets
env:
RELEASE_TAG: ${{ inputs.release_tag }}
SOURCE_GITHUB_TOKEN: ${{ secrets.SHPIT_GH_TOKEN }}
TARGET_GITHUB_TOKEN: ${{ github.token }}
TARGET_REPOSITORY: ${{ github.repository }}
run: ./scripts/publish-tabex-release.sh "${RELEASE_TAG}"

- name: Trigger pkgbuilds metadata bump
env:
GH_TOKEN: ${{ github.token }}
run: gh workflow run version-bumps.yml --repo "${{ github.repository }}" --ref main
4 changes: 4 additions & 0 deletions .github/workflows/version-bumps.yml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ permissions:
contents: write
pull-requests: write

concurrency:
group: version-bumps
cancel-in-progress: false

jobs:
update:
runs-on: ubuntu-latest
Expand Down
14 changes: 7 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,9 @@ Arch Linux package definitions for SHPIT-maintained command-line tools.
| Package | Upstream | Notes |
|---|---|---|
| `foundry-cli-bin` | `shpitdev/foundry-cli` GitHub Releases | Private release assets. Same auth model as the other SHPIT packages. |
| `meshix-cli-bin` | `shpitdev/meshix-observability` GitHub Releases | Private release assets. Same auth model as `tabex-bin` and `osyrra-bin`. |
| `tabex-bin` | `shpitdev/tabex` GitHub Releases | Private release assets. The PKGBUILD is public, but `makepkg` needs GitHub access to the `shpitdev` org to download the release tarball. |
| `osyrra-bin` | `shpitdev/osyrra` GitHub Releases | Private release assets. Same auth model as `tabex-bin`. |
| `meshix-cli-bin` | `shpitdev/meshix-observability` GitHub Releases | Private release assets. Same auth model as `osyrra-bin`. |
| `tabex-bin` | `shpitdev/pkgbuilds` GitHub Releases | Public binary release assets mirrored from the private Tabex release after digest and archive verification. No GitHub credentials are required to install a mirrored version. |
| `osyrra-bin` | `shpitdev/osyrra` GitHub Releases | Private release assets. Same auth model as `meshix-cli-bin`. |

## Automation

Expand Down Expand Up @@ -38,7 +38,7 @@ cd <package-dir>
makepkg -si
```

`gh auth login` must be configured with access to the `shpitdev` org before `makepkg` can download the private `foundry-cli-bin`, `meshix-cli-bin`, `tabex-bin`, or `osyrra-bin` release assets.
`gh auth login` must be configured with access to the `shpitdev` org before `makepkg` can download the private `foundry-cli-bin`, `meshix-cli-bin`, or `osyrra-bin` release assets. Tabex versions published through the public binary channel need no GitHub credentials.

After installing `tabex-bin`, start with:

Expand All @@ -52,12 +52,12 @@ The package includes an install hook that prints the same guidance after install

- You can use this repo immediately without creating the AUR repositories or AUR secrets.
- The scheduled/manual bump workflow uses the repository `GITHUB_TOKEN` for branch and PR operations in this repo.
- Without `SHPIT_GH_TOKEN`, the workflow skips the private package updates (`foundry-cli-bin`, `meshix-cli-bin`, `tabex-bin`, and `osyrra-bin`).
- Without `SHPIT_GH_TOKEN`, the workflow skips the private package updates (`foundry-cli-bin`, `meshix-cli-bin`, and `osyrra-bin`). Public Tabex package updates remain available.
- Without AUR secrets, the publish workflow exits successfully without pushing anywhere.

## Secrets

- `SHPIT_GH_TOKEN` — optional; required for GitHub Actions to refresh the private SHPIT packages (`foundry-cli-bin`, `meshix-cli-bin`, `tabex-bin`, and `osyrra-bin`) from their GitHub releases.
- `SHPIT_GH_TOKEN` — optional for routine version bumps; required by the trusted `publish-tabex-release` workflow only to read an exact stable release from the private Tabex repository. The workflow uses its repository-scoped token to trigger the local package bump only after the public mirror verifies. The secret is also required to refresh the other private SHPIT packages.
- `AUR_USERNAME`, `AUR_EMAIL`, `AUR_SSH_PRIVATE_KEY` — optional until you actually want to publish to AUR.

## Local Auth
Expand All @@ -76,7 +76,7 @@ The package includes an install hook that prints the same guidance after install

1. Create the GitHub repository and enable Actions.
2. In `Settings -> Actions -> General`, set workflow permissions to read and write, and enable GitHub Actions to create pull requests.
3. Attach the `SHPIT_GH_TOKEN` secret (org-level or repo-level) to this repo so the bump workflow can read the private release assets.
3. Attach the `SHPIT_GH_TOKEN` secret (org-level or repo-level) to this repo so the Tabex publisher and private-package bump paths can read their private release assets.
4. When the AUR repos exist, add `AUR_USERNAME`, `AUR_EMAIL`, and `AUR_SSH_PRIVATE_KEY`.
5. Run `version-bumps` manually once, confirm the PR output, then merge.
6. After the first merge, `publish.yml` will start pushing package updates to AUR only if those AUR secrets are present.
10 changes: 6 additions & 4 deletions docs/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ Use this first.
- set workflow permissions to `Read and write`
- enable `Allow GitHub Actions to create and approve pull requests`
4. Do not add any AUR secrets yet.
5. Attach `SHPIT_GH_TOKEN` if you want Actions to bump the private SHPIT packages.
5. Attach `SHPIT_GH_TOKEN` if you want Actions to bump private SHPIT packages or publish verified Tabex binaries from the private source release.
6. Run the `version-bumps` workflow manually.

Result:

- branch and PR creation use the repo `GITHUB_TOKEN`
- `meshix-cli-bin`, `tabex-bin`, and `osyrra-bin` update only if the repo has access to `SHPIT_GH_TOKEN`
- `meshix-cli-bin` and `osyrra-bin` update only if the repo has access to `SHPIT_GH_TOKEN`
- `tabex-bin` updates anonymously after an exact stable Tabex release has been mirrored publicly
- AUR publishing is skipped without failing
- upstream `meshix-observability`, `tabex`, and `osyrra` release workflows can also trigger this workflow automatically with `gh workflow run version-bumps.yml`, but that depends on `SHPIT_WORKFLOW_DISPATCH_TOKEN` being available in their producer-repo Depot CI secrets
- upstream `meshix-observability` and `osyrra` release workflows can trigger the version-bump workflow directly
- the Tabex release workflow instead dispatches `publish-tabex-release.yml`; that trusted workflow verifies and publishes the exact public mirror before it triggers the local version bump

## GitHub UI Links

Expand All @@ -28,7 +30,7 @@ Result:

## SHPIT_GH_TOKEN

Create the secret (org-level or repo-level) with access to read private releases on `shpitdev/meshix-observability`, `shpitdev/tabex`, and `shpitdev/osyrra`. An org-level secret with `selected` visibility is the cleanest option if you have multiple consuming repos.
Create the secret (org-level or repo-level) with access to read private releases on `shpitdev/meshix-observability`, `shpitdev/tabex`, and `shpitdev/osyrra`. The Tabex publisher uses it only for the private source read; publication and the same-repository bump dispatch use the workflow's repository-scoped token. An org-level secret with `selected` visibility is the cleanest option if you have multiple consuming repos.

Attach it to this repo with:

Expand Down
158 changes: 158 additions & 0 deletions scripts/publish-tabex-release.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
#!/usr/bin/env bash
set -euo pipefail

if (($# != 1)); then
echo "usage: $0 <stable-release-tag>" >&2
exit 1
fi

release_tag="$1"
source_repo="shpitdev/tabex"
target_repo="${TARGET_REPOSITORY:-shpitdev/pkgbuilds}"

if [[ ! "${release_tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Refusing to publish a non-stable Tabex release tag: ${release_tag}" >&2
exit 1
fi
if [[ -z "${SOURCE_GITHUB_TOKEN:-}" ]]; then
echo "SOURCE_GITHUB_TOKEN is required to read the private Tabex release." >&2
exit 1
fi
if [[ -z "${TARGET_GITHUB_TOKEN:-}" ]]; then
echo "TARGET_GITHUB_TOKEN is required to publish the public binary release." >&2
exit 1
fi

version="${release_tag#v}"
public_tag="tabex-${release_tag}"
assets=(
"tabex_${release_tag}_darwin_arm64.tar.gz"
"tabex_${release_tag}_linux_amd64.tar.gz"
)
expected_names_json="$(printf '%s\n' "${assets[@]}" | jq -R . | jq -s .)"
workdir="$(mktemp -d)"
trap 'rm -rf "${workdir}"' EXIT

verify_public_release() {
local public_release_file="$1"
local asset
local public_digest
local source_digest

if ! jq -e --arg public_tag "${public_tag}" --argjson expected_names "${expected_names_json}" '
.tag_name == $public_tag
and .draft == false
and .prerelease == false
and ([.assets[].name] | sort) == ($expected_names | sort)
and all(.assets[]; (.digest // "") | test("^sha256:[0-9a-f]{64}$"))
' "${public_release_file}" >/dev/null; then
echo "Public release ${public_tag} is incomplete or contains unexpected assets." >&2
return 1
fi

for asset in "${assets[@]}"; do
source_digest="$(jq -r --arg name "${asset}" '.assets[] | select(.name == $name) | .digest' "${source_release}")"
public_digest="$(jq -r --arg name "${asset}" '.assets[] | select(.name == $name) | .digest' "${public_release_file}")"
if [[ "${public_digest}" != "${source_digest}" ]]; then
echo "Public release ${public_tag} differs from the private source asset ${asset}." >&2
return 1
fi
done
}

source_release="${workdir}/source-release.json"
GH_TOKEN="${SOURCE_GITHUB_TOKEN}" gh api \
"repos/${source_repo}/releases/tags/${release_tag}" > "${source_release}"

if ! jq -e --arg tag "${release_tag}" '
.tag_name == $tag and .draft == false and .prerelease == false
' "${source_release}" >/dev/null; then
echo "The source must be an exact, published, stable Tabex release: ${release_tag}" >&2
exit 1
fi

for asset in "${assets[@]}"; do
digest="$(jq -r --arg name "${asset}" '
[.assets[] | select(.name == $name)] as $matches
| if ($matches | length) == 1 then $matches[0].digest // empty else empty end
' "${source_release}")"
if [[ ! "${digest}" =~ ^sha256:[0-9a-f]{64}$ ]]; then
echo "The source release must contain one digested asset named ${asset}." >&2
exit 1
fi

GH_TOKEN="${SOURCE_GITHUB_TOKEN}" gh release download "${release_tag}" \
--repo "${source_repo}" \
--pattern "${asset}" \
--dir "${workdir}" >/dev/null

actual_digest="$(sha256sum "${workdir}/${asset}" | awk '{print $1}')"
if [[ "sha256:${actual_digest}" != "${digest}" ]]; then
echo "SHA-256 mismatch for private release asset ${asset}." >&2
exit 1
fi

archive_root="${asset%.tar.gz}"
archive_entries="$(tar -tzf "${workdir}/${asset}")"
expected_entries="${archive_root}/
${archive_root}/tabex"
if [[ "${archive_entries}" != "${expected_entries}" ]]; then
echo "Unexpected archive contents in ${asset}; refusing to publish." >&2
exit 1
fi

binary_entry_type="$(tar -tvzf "${workdir}/${asset}" | awk -v path="${archive_root}/tabex" '$NF == path { print substr($1, 1, 1) }')"
if [[ "${binary_entry_type}" != "-" ]]; then
echo "The Tabex entry in ${asset} is not a regular file; refusing to publish." >&2
exit 1
fi

extract_dir="${workdir}/extract-${asset}"
mkdir -p "${extract_dir}"
tar -xzf "${workdir}/${asset}" -C "${extract_dir}"
if [[ ! -f "${extract_dir}/${archive_root}/tabex" || ! -x "${extract_dir}/${archive_root}/tabex" ]]; then
echo "The Tabex binary in ${asset} is not executable; refusing to publish." >&2
exit 1
fi
done

public_release="${workdir}/public-release.json"
public_release_error="${workdir}/public-release.error"
if GH_TOKEN="${TARGET_GITHUB_TOKEN}" gh api \
"repos/${target_repo}/releases/tags/${public_tag}" \
> "${public_release}" 2> "${public_release_error}"; then
verify_public_release "${public_release}"
echo "Public Tabex binaries already match ${release_tag}; nothing to publish."
exit 0
fi

if ! grep -q 'HTTP 404' "${public_release_error}"; then
cat "${public_release_error}" >&2
exit 1
fi

notes_file="${workdir}/release-notes.md"
cat > "${notes_file}" <<EOF
Public Tabex ${version} native-host binaries for package-manager installation.

These archives are copied byte-for-byte from the authenticated Tabex ${release_tag} release after GitHub's SHA-256 digests and archive layout are verified. This release contains binaries only; the Tabex source repository remains private.
EOF

release_files=()
for asset in "${assets[@]}"; do
release_files+=("${workdir}/${asset}")
done

GH_TOKEN="${TARGET_GITHUB_TOKEN}" gh release create "${public_tag}" \
"${release_files[@]}" \
--repo "${target_repo}" \
--target main \
--title "Tabex ${release_tag} public binaries" \
--notes-file "${notes_file}" \
--latest=false

GH_TOKEN="${TARGET_GITHUB_TOKEN}" gh api \
"repos/${target_repo}/releases/tags/${public_tag}" > "${public_release}"
verify_public_release "${public_release}"

echo "Published ${target_repo} release ${public_tag}."
103 changes: 103 additions & 0 deletions scripts/publish-tabex-release.test.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
#!/usr/bin/env bash
set -euo pipefail

repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)"
workdir="$(mktemp -d)"
trap 'rm -rf "${workdir}"' EXIT
mkdir -p "${workdir}/bin" "${workdir}/assets"

release_tag="v9.8.7"
for platform in darwin_arm64 linux_amd64; do
archive_root="tabex_${release_tag}_${platform}"
mkdir -p "${workdir}/stage/${archive_root}"
printf '#!/usr/bin/env bash\n' > "${workdir}/stage/${archive_root}/tabex"
chmod +x "${workdir}/stage/${archive_root}/tabex"
tar -czf "${workdir}/assets/${archive_root}.tar.gz" -C "${workdir}/stage" "${archive_root}"
done

darwin_sha="$(sha256sum "${workdir}/assets/tabex_${release_tag}_darwin_arm64.tar.gz" | awk '{print $1}')"
linux_sha="$(sha256sum "${workdir}/assets/tabex_${release_tag}_linux_amd64.tar.gz" | awk '{print $1}')"
jq -n \
--arg tag "${release_tag}" \
--arg darwin_sha "${darwin_sha}" \
--arg linux_sha "${linux_sha}" \
'{
tag_name: $tag,
draft: false,
prerelease: false,
assets: [
{name: ("tabex_" + $tag + "_darwin_arm64.tar.gz"), digest: ("sha256:" + $darwin_sha)},
{name: ("tabex_" + $tag + "_linux_amd64.tar.gz"), digest: ("sha256:" + $linux_sha)}
]
}' > "${workdir}/source-release.json"

cat > "${workdir}/bin/gh" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

if [[ "$1" == "api" && "$2" == repos/shpitdev/tabex/releases/tags/* ]]; then
cat "${TABEX_SOURCE_RELEASE_FIXTURE}"
exit 0
fi
if [[ "$1" == "api" && "$2" == repos/shpitdev/pkgbuilds/releases/tags/* ]]; then
if [[ -f "${TABEX_PUBLIC_RELEASE_MARKER}" ]]; then
jq '.tag_name = "tabex-v9.8.7"' "${TABEX_SOURCE_RELEASE_FIXTURE}"
exit 0
fi
echo 'gh: Not Found (HTTP 404)' >&2
exit 1
fi
if [[ "$1" == "release" && "$2" == "download" ]]; then
asset=""
output_dir=""
while (($#)); do
case "$1" in
--pattern)
asset="$2"
shift 2
;;
--dir)
output_dir="$2"
shift 2
;;
*)
shift
;;
esac
done
cp "${TABEX_SOURCE_ASSET_DIR}/${asset}" "${output_dir}/${asset}"
exit 0
fi
if [[ "$1" == "release" && "$2" == "create" ]]; then
printf '%q ' "$@" > "${TABEX_CREATE_LOG}"
touch "${TABEX_PUBLIC_RELEASE_MARKER}"
exit 0
fi

echo "Unexpected gh invocation: $*" >&2
exit 1
EOF
chmod +x "${workdir}/bin/gh"

if PATH="${workdir}/bin:${PATH}" \
"${repo_root}/scripts/publish-tabex-release.sh" "v9.8.7-rc.1" >/dev/null 2>&1; then
echo "Publisher accepted a prerelease tag." >&2
exit 1
fi

PATH="${workdir}/bin:${PATH}" \
SOURCE_GITHUB_TOKEN=source-token \
TARGET_GITHUB_TOKEN=target-token \
TARGET_REPOSITORY=shpitdev/pkgbuilds \
TABEX_SOURCE_RELEASE_FIXTURE="${workdir}/source-release.json" \
TABEX_SOURCE_ASSET_DIR="${workdir}/assets" \
TABEX_CREATE_LOG="${workdir}/create.log" \
TABEX_PUBLIC_RELEASE_MARKER="${workdir}/public-release-created" \
"${repo_root}/scripts/publish-tabex-release.sh" "${release_tag}"

grep -Fq 'tabex-v9.8.7' "${workdir}/create.log"
grep -Fq -- '--latest=false' "${workdir}/create.log"
if grep -Fq -- '--clobber' "${workdir}/create.log"; then
echo "Publisher must not overwrite existing public assets." >&2
exit 1
fi
Loading