diff --git a/.github/workflows/version-bumps.yml b/.github/workflows/version-bumps.yml index dd06371..bec61d8 100644 --- a/.github/workflows/version-bumps.yml +++ b/.github/workflows/version-bumps.yml @@ -6,6 +6,9 @@ on: meshix_cli_version: description: Optional Meshix CLI release tag to pin for this run (for example v0.0.2) required: false + foundry_cli_release_tag: + description: Optional Foundry CLI release tag to pin for this run (for example v0.0.30) + required: false schedule: - cron: '23 6 * * *' @@ -22,6 +25,7 @@ jobs: GH_TOKEN: ${{ github.token }} SHPIT_GH_TOKEN: ${{ secrets.SHPIT_GH_TOKEN }} MESHIX_CLI_VERSION: ${{ github.event.inputs.meshix_cli_version || '' }} + FOUNDRY_CLI_RELEASE_TAG: ${{ github.event.inputs.foundry_cli_release_tag || '' }} UPDATE_BRANCH: automation/version-bumps steps: - name: Install updater dependencies @@ -56,7 +60,7 @@ jobs: if: ${{ steps.detect.outputs.changed == 'true' }} run: | git checkout -B "${UPDATE_BRANCH}" - git add README.md docs scripts .github/workflows meshix-cli-bin tabex-bin osyrra-bin + git add README.md docs scripts .github/workflows meshix-cli-bin foundry-cli-bin tabex-bin osyrra-bin git commit -m "chore(pkgbuilds): bump package versions" git push --force --set-upstream origin "${UPDATE_BRANCH}" diff --git a/README.md b/README.md index 49475f4..d1021ea 100644 --- a/README.md +++ b/README.md @@ -6,6 +6,7 @@ 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`. | @@ -37,7 +38,7 @@ cd makepkg -si ``` -`gh auth login` must be configured with access to the `shpitdev` org before `makepkg` can download the private `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`, `tabex-bin`, or `osyrra-bin` release assets. After installing `tabex-bin`, start with: @@ -51,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 (`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`, `tabex-bin`, and `osyrra-bin`). - 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 (`meshix-cli-bin`, `tabex-bin`, and `osyrra-bin`) from their GitHub releases. +- `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. - `AUR_USERNAME`, `AUR_EMAIL`, `AUR_SSH_PRIVATE_KEY` — optional until you actually want to publish to AUR. ## Local Auth diff --git a/foundry-cli-bin/.SRCINFO b/foundry-cli-bin/.SRCINFO new file mode 100644 index 0000000..27a0256 --- /dev/null +++ b/foundry-cli-bin/.SRCINFO @@ -0,0 +1,13 @@ +pkgbase = foundry-cli-bin + pkgdesc = Foundry DevOps automation CLI + pkgver = 0.0.30 + pkgrel = 1 + url = https://github.com/shpitdev/foundry-cli + install = foundry-cli-bin.install + arch = x86_64 + license = LicenseRef-proprietary + makedepends = github-cli + provides = foundry-cli + conflicts = foundry-cli + +pkgname = foundry-cli-bin diff --git a/foundry-cli-bin/PKGBUILD b/foundry-cli-bin/PKGBUILD new file mode 100644 index 0000000..c70eeef --- /dev/null +++ b/foundry-cli-bin/PKGBUILD @@ -0,0 +1,50 @@ +# Maintainer: Anand Pant + +pkgname=foundry-cli-bin +pkgver=0.0.30 +pkgrel=1 +pkgdesc="Foundry DevOps automation CLI" +arch=('x86_64') +url="https://github.com/shpitdev/foundry-cli" +license=('LicenseRef-proprietary') +install="${pkgname}.install" +makedepends=('github-cli') +provides=('foundry-cli') +conflicts=('foundry-cli') + +_asset="foundry-cli_${pkgver}_linux_amd64.tar.gz" +_sha256='918ed7552f9529dbaaa92697c80b3586e97e148ded86f94ad7a8469e8953b574' + +prepare() { + gh release download "v${pkgver}" \ + --repo shpitdev/foundry-cli \ + --pattern "${_asset}" \ + --dir . --clobber + + echo "${_sha256} ${_asset}" | sha256sum -c + tar xzf "${_asset}" +} + +package() { + install -dm755 "${pkgdir}/usr/lib/foundry-cli" + install -Dm755 "foundry-cli" \ + "${pkgdir}/usr/lib/foundry-cli/foundry-cli" + cp -R templates "${pkgdir}/usr/lib/foundry-cli/" + + if [[ ! -f "${pkgdir}/usr/lib/foundry-cli/templates/README.md" ]]; then + install -Dm644 /dev/stdin \ + "${pkgdir}/usr/lib/foundry-cli/templates/README.md" <<'EOT' +# templates +EOT + fi + + install -Dm644 LICENSE "${pkgdir}/usr/share/licenses/${pkgname}/LICENSE" + if [[ -f NOTICE ]]; then + install -Dm644 NOTICE "${pkgdir}/usr/share/licenses/${pkgname}/NOTICE" + fi + install -Dm644 README.md "${pkgdir}/usr/share/doc/${pkgname}/README.md" + + install -dm755 "${pkgdir}/usr/bin" + ln -s ../lib/foundry-cli/foundry-cli \ + "${pkgdir}/usr/bin/foundry-cli" +} diff --git a/foundry-cli-bin/foundry-cli-bin.install b/foundry-cli-bin/foundry-cli-bin.install new file mode 100644 index 0000000..6386d7e --- /dev/null +++ b/foundry-cli-bin/foundry-cli-bin.install @@ -0,0 +1,14 @@ +post_install() { + cat <<'MSG' +==> foundry-cli-bin: package-manager installs do not edit your shell config. +==> To add shell completion in zsh: + printf '\nsource <(foundry-cli completion --code zsh)\n' >> ~/.zshrc +==> To add it in bash: + printf '\nsource <(foundry-cli completion --code bash)\n' >> ~/.bashrc +==> Restart your shell after adding the snippet. +MSG +} + +post_upgrade() { + post_install +} diff --git a/scripts/select-foundry-cli-asset.sh b/scripts/select-foundry-cli-asset.sh new file mode 100755 index 0000000..3d24255 --- /dev/null +++ b/scripts/select-foundry-cli-asset.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +if (($# != 1)); then + echo "usage: $0 " >&2 + exit 1 +fi + +pkgver="$1" +expected_asset="foundry-cli_${pkgver}_linux_amd64.tar.gz" +candidate_assets="$(jq -c ' + .assets + | map(select(.name | test("^foundry-cli_.*_linux_amd64\\.tar\\.gz$"))) +')" +candidate_count="$(jq 'length' <<<"${candidate_assets}")" +asset_name="$(jq -r '.[0].name // empty' <<<"${candidate_assets}")" + +if [[ "${candidate_count}" -ne 1 || "${asset_name}" != "${expected_asset}" ]]; then + echo "foundry-cli release must contain exactly ${expected_asset}; found ${candidate_count} matching candidates" >&2 + exit 1 +fi + +jq -c '.[0]' <<<"${candidate_assets}" diff --git a/scripts/select-foundry-cli-asset.test.sh b/scripts/select-foundry-cli-asset.test.sh new file mode 100755 index 0000000..8c24681 --- /dev/null +++ b/scripts/select-foundry-cli-asset.test.sh @@ -0,0 +1,23 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +selector="${repo_root}/scripts/select-foundry-cli-asset.sh" + +expect_rejected() { + local release_json="$1" + if "${selector}" 1.2.3 <<<"${release_json}" >/dev/null 2>&1; then + echo "Expected release assets to be rejected: ${release_json}" >&2 + exit 1 + fi +} + +selected="$("${selector}" 1.2.3 <<'JSON' +{"assets":[{"name":"foundry-cli_1.2.3_linux_amd64.tar.gz","digest":"sha256:good"}]} +JSON +)" +[[ "$(jq -r '.name' <<<"${selected}")" == "foundry-cli_1.2.3_linux_amd64.tar.gz" ]] + +expect_rejected '{"assets":[]}' +expect_rejected '{"assets":[{"name":"foundry-cli_9.9.9_linux_amd64.tar.gz","digest":"sha256:wrong"}]}' +expect_rejected '{"assets":[{"name":"foundry-cli_1.2.3_linux_amd64.tar.gz","digest":"sha256:one"},{"name":"foundry-cli_9.9.9_linux_amd64.tar.gz","digest":"sha256:two"}]}' diff --git a/scripts/update-foundry-cli-bin.sh b/scripts/update-foundry-cli-bin.sh new file mode 100755 index 0000000..292f1be --- /dev/null +++ b/scripts/update-foundry-cli-bin.sh @@ -0,0 +1,116 @@ +#!/usr/bin/env bash +set -euo pipefail + +optional=false +if (($# > 1)); then + echo "usage: $0 [--optional]" >&2 + exit 1 +fi +if (($# == 1)); then + if [[ "$1" != "--optional" ]]; then + echo "usage: $0 [--optional]" >&2 + exit 1 + fi + optional=true +fi + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +package_dir="${repo_root}/foundry-cli-bin" +pkgbuild="${package_dir}/PKGBUILD" +repo="shpitdev/foundry-cli" +asset_prefix="foundry-cli" +release_tag="${FOUNDRY_CLI_RELEASE_TAG:-}" + +fetch_release_by_tag() { + local tag="$1" + + if [[ -n "${SHPIT_GH_TOKEN:-}" ]]; then + GH_TOKEN="${SHPIT_GH_TOKEN}" gh api "repos/${repo}/releases/tags/${tag}" + else + gh api "repos/${repo}/releases/tags/${tag}" + fi +} + +fetch_release_with_assets() { + local include_prereleases="$1" + + if [[ -n "${SHPIT_GH_TOKEN:-}" ]]; then + GH_TOKEN="${SHPIT_GH_TOKEN}" gh api --paginate "repos/${repo}/releases" + else + gh api --paginate "repos/${repo}/releases" + fi | jq -s -c --arg asset_prefix "${asset_prefix}" --argjson include_prereleases "${include_prereleases}" ' + add + | map(select(.draft | not)) + | map(select($include_prereleases or (.prerelease | not))) + | map(select(any(.assets[]?; (.name | test("^" + $asset_prefix + "_.*_linux_amd64\\.tar\\.gz$"))))) + | first // empty + ' +} + +if [[ -n "${release_tag}" ]]; then + release_json="$(fetch_release_by_tag "${release_tag}")" +elif [[ -n "${SHPIT_GH_TOKEN:-}" || -z "${GITHUB_ACTIONS:-}" ]]; then + release_json="$(fetch_release_with_assets false)" + if [[ -z "${release_json}" || "${release_json}" == "null" ]]; then + release_json="$(fetch_release_with_assets true)" + fi +elif [[ -n "${GITHUB_ACTIONS:-}" ]]; then + if [[ "${optional}" == "true" ]]; then + echo "Skipping foundry-cli-bin: SHPIT_GH_TOKEN is not configured in GitHub Actions." >&2 + exit 0 + fi + echo "SHPIT_GH_TOKEN is required in GitHub Actions to read the private foundry-cli release." >&2 + exit 1 +fi + +if [[ -z "${release_json}" || "${release_json}" == "null" ]]; then + if [[ "${optional}" == "true" ]]; then + echo "Skipping foundry-cli-bin: no release contains foundry-cli linux amd64 archives." >&2 + exit 0 + fi + echo "foundry-cli has no release with linux amd64 archives" >&2 + exit 1 +fi + +tag_name="$(jq -r '.tag_name' <<<"${release_json}")" +pkgver="${tag_name#v}" +if ! asset_json="$("${repo_root}/scripts/select-foundry-cli-asset.sh" "${pkgver}" <<<"${release_json}")"; then + if [[ "${optional}" == "true" ]]; then + echo "Skipping foundry-cli-bin: selected release has invalid linux amd64 assets." >&2 + exit 0 + fi + exit 1 +fi +asset_name="$(jq -r '.name' <<<"${asset_json}")" +sha256="$(jq -r '.digest // empty' <<<"${asset_json}")" + +if [[ -z "${sha256}" || "${sha256}" == "null" ]]; then + if [[ "${optional}" == "true" ]]; then + echo "Skipping foundry-cli-bin: selected release is missing an asset digest." >&2 + exit 0 + fi + echo "foundry-cli selected release is missing an asset digest" >&2 + exit 1 +fi + +sha256="${sha256#sha256:}" +mkdir -p "${repo_root}/.memory" +tmpdir="$(mktemp -d "${repo_root}/.memory/update-foundry-cli-bin.XXXXXX")" +trap 'rm -rf "${tmpdir}"' EXIT + +if [[ -n "${SHPIT_GH_TOKEN:-}" ]]; then + GH_TOKEN="${SHPIT_GH_TOKEN}" gh release download "v${pkgver}" --repo "${repo}" --pattern "${asset_name}" --dir "${tmpdir}" --clobber >/dev/null +else + gh release download "v${pkgver}" --repo "${repo}" --pattern "${asset_name}" --dir "${tmpdir}" --clobber >/dev/null +fi + +( + cd "${tmpdir}" + echo "${sha256} ${asset_name}" | sha256sum -c + "${repo_root}/scripts/validate-foundry-cli-archive.sh" "${asset_name}" +) + +perl -0pi -e "s/^pkgver=.*/pkgver=${pkgver}/m" "${pkgbuild}" +perl -0pi -e "s/^_sha256=.*/_sha256='${sha256}'/m" "${pkgbuild}" + +"${repo_root}/scripts/render-srcinfo.sh" "${package_dir}" diff --git a/scripts/update-packages.sh b/scripts/update-packages.sh index a20f20c..b795955 100755 --- a/scripts/update-packages.sh +++ b/scripts/update-packages.sh @@ -8,7 +8,7 @@ if (($# == 0)); then fi if [[ "$1" == "auto" ]]; then - packages=(meshix-cli-bin) + packages=(meshix-cli-bin foundry-cli-bin) if [[ -n "${SHPIT_GH_TOKEN:-}" || -z "${GITHUB_ACTIONS:-}" ]]; then packages+=(tabex-bin) packages+=(osyrra-bin) @@ -16,6 +16,7 @@ if [[ "$1" == "auto" ]]; then elif [[ "$1" == "all" ]]; then packages=( meshix-cli-bin + foundry-cli-bin tabex-bin osyrra-bin ) @@ -32,6 +33,13 @@ for package in "${packages[@]}"; do "${repo_root}/scripts/update-meshix-cli-bin.sh" fi ;; + foundry-cli-bin) + if [[ "$1" == "auto" ]]; then + "${repo_root}/scripts/update-foundry-cli-bin.sh" --optional + else + "${repo_root}/scripts/update-foundry-cli-bin.sh" + fi + ;; tabex-bin) if [[ "$1" == "auto" ]]; then "${repo_root}/scripts/update-tabex-bin.sh" --optional diff --git a/scripts/validate-foundry-cli-archive.sh b/scripts/validate-foundry-cli-archive.sh new file mode 100755 index 0000000..a3d32d2 --- /dev/null +++ b/scripts/validate-foundry-cli-archive.sh @@ -0,0 +1,49 @@ +#!/usr/bin/env bash +set -euo pipefail + +if (($# != 1)); then + echo "usage: $0 " >&2 + exit 1 +fi + +archive="$1" +listing="$(tar -tzf "${archive}")" + +while IFS= read -r member; do + normalized="${member%/}" + if [[ -z "${normalized}" || "${normalized}" == /* || "${normalized}" == *\\* ]]; then + echo "Release archive ${archive} contains an absolute or invalid member path: ${member}" >&2 + exit 1 + fi + + IFS='/' read -r -a segments <<<"${normalized}" + for segment in "${segments[@]}"; do + if [[ -z "${segment}" || "${segment}" == "." || "${segment}" == ".." ]]; then + echo "Release archive ${archive} contains a non-normal member path: ${member}" >&2 + exit 1 + fi + done +done <<<"${listing}" + +while IFS= read -r mode _; do + case "${mode:0:1}" in + -|d) ;; + l|h) + echo "Release archive ${archive} contains a link; symlink and hardlink targets are not trusted." >&2 + exit 1 + ;; + *) + echo "Release archive ${archive} contains an unsafe member type: ${mode:0:1}" >&2 + exit 1 + ;; + esac +done < <(tar -tvzf "${archive}") + +grep -Fxq "foundry-cli" <<<"${listing}" +grep -Fxq "LICENSE" <<<"${listing}" +grep -Fxq "README.md" <<<"${listing}" + +if ! grep -Eq '^templates/(compute-module-ts|compute-modules/typescript)/package\.json$' <<<"${listing}"; then + echo "Release archive ${archive} is missing a recognized compute module template package.json." >&2 + exit 1 +fi diff --git a/scripts/validate-foundry-cli-archive.test.sh b/scripts/validate-foundry-cli-archive.test.sh new file mode 100755 index 0000000..f942c4d --- /dev/null +++ b/scripts/validate-foundry-cli-archive.test.sh @@ -0,0 +1,58 @@ +#!/usr/bin/env bash +set -euo pipefail + +repo_root="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)" +validator="${repo_root}/scripts/validate-foundry-cli-archive.sh" +mkdir -p "${repo_root}/.memory" +test_root="$(mktemp -d "${repo_root}/.memory/validate-foundry-cli-archive.XXXXXX")" +trap 'rm -rf "${test_root}"' EXIT + +make_base_fixture() { + local root="$1" + mkdir -p "${root}/templates/compute-modules/typescript" + touch "${root}/foundry-cli" "${root}/LICENSE" "${root}/README.md" + touch "${root}/templates/compute-modules/typescript/package.json" +} + +expect_rejected() { + local archive="$1" + if "${validator}" "${archive}" >/dev/null 2>&1; then + echo "Expected archive to be rejected: ${archive}" >&2 + exit 1 + fi +} + +mkdir -p "${test_root}/good" +make_base_fixture "${test_root}/good" +tar -C "${test_root}/good" -czf "${test_root}/good.tar.gz" \ + foundry-cli LICENSE README.md templates +"${validator}" "${test_root}/good.tar.gz" + +tar -C "${test_root}/good" -czf "${test_root}/absolute.tar.gz" \ + --transform='s#^#/absolute/#' foundry-cli LICENSE README.md templates +expect_rejected "${test_root}/absolute.tar.gz" + +tar -C "${test_root}/good" -czf "${test_root}/traversal.tar.gz" \ + --transform='s#^#../#' foundry-cli LICENSE README.md templates +expect_rejected "${test_root}/traversal.tar.gz" + +mkdir -p "${test_root}/symlink" +make_base_fixture "${test_root}/symlink" +ln -s ../../outside "${test_root}/symlink/escape" +tar -C "${test_root}/symlink" -czf "${test_root}/symlink.tar.gz" \ + foundry-cli LICENSE README.md templates escape +expect_rejected "${test_root}/symlink.tar.gz" + +mkdir -p "${test_root}/hardlink" +make_base_fixture "${test_root}/hardlink" +ln "${test_root}/hardlink/foundry-cli" "${test_root}/hardlink/escape" +tar -C "${test_root}/hardlink" -czf "${test_root}/hardlink.tar.gz" \ + foundry-cli LICENSE README.md templates escape +expect_rejected "${test_root}/hardlink.tar.gz" + +mkdir -p "${test_root}/fifo" +make_base_fixture "${test_root}/fifo" +mkfifo "${test_root}/fifo/unsafe" +tar -C "${test_root}/fifo" -czf "${test_root}/fifo.tar.gz" \ + foundry-cli LICENSE README.md templates unsafe +expect_rejected "${test_root}/fifo.tar.gz" diff --git a/scripts/validate-packages.sh b/scripts/validate-packages.sh index f08fbd5..8351b28 100755 --- a/scripts/validate-packages.sh +++ b/scripts/validate-packages.sh @@ -9,6 +9,9 @@ for package_dir in "${repo_root}"/*; do "${repo_root}/scripts/validate-package.sh" "${package_dir}" done +"${repo_root}/scripts/validate-foundry-cli-archive.test.sh" +"${repo_root}/scripts/select-foundry-cli-asset.test.sh" + meshix_pkg="${repo_root}/meshix-cli-bin" if [[ -f "${meshix_pkg}/PKGBUILD" ]]; then grep -q 'gh release download "meshix-cli-v${pkgver}"' "${meshix_pkg}/PKGBUILD"