Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 commits
Select commit Hold shift + click to select a range
09f97c1
ci: enable ARM builds
zerosnacks Jan 21, 2026
bc096e4
feat: expand test matrix to match release matrix (#23)
zerosnacks Jan 21, 2026
35c9333
feat: add build attestations and verification
zerosnacks Jan 21, 2026
49a1058
test: skip attestation verification in installer test
zerosnacks Jan 21, 2026
0e9aa70
fix: use try_download for attestations to avoid exit on 404
zerosnacks Jan 21, 2026
d0bea6b
temporarily disable -arm until release is ready
zerosnacks Jan 21, 2026
4c41da6
rename FOUNDRYUP_SKIP_VERIFY to FOUNDRYUP_IGNORE_VERIFICATION
zerosnacks Jan 21, 2026
a5c55b2
Merge branch 'enable-arm-ci' into feat/attestations
zerosnacks Jan 21, 2026
87864e6
fix: use set -eo pipefail instead of set -u for Windows compatibility
zerosnacks Jan 21, 2026
52cf5be
add shellcheck
zerosnacks Jan 21, 2026
f3311a9
just use bash
zerosnacks Jan 21, 2026
8d6f406
fix: update shebang test to expect bash
zerosnacks Jan 21, 2026
d8f35c2
fix: use bash instead of sh in tests
zerosnacks Jan 21, 2026
c660bda
fix: use Git Bash for installer script tests on Windows
zerosnacks Jan 21, 2026
af6b61d
fix: add debug output and strip pipefail for Windows compatibility
zerosnacks Jan 21, 2026
481bcb4
fixes
zerosnacks Jan 21, 2026
ed65a28
fix: normalize CRLF to LF in script tests for Windows
zerosnacks Jan 21, 2026
c09bb38
fix: write script to temp file instead of passing via -c argument
zerosnacks Jan 21, 2026
74c5499
fix: enforce LF line endings for .rs and .sh files
zerosnacks Jan 21, 2026
e501c36
fix: use temp file for all script tests to avoid Windows shell issues
zerosnacks Jan 21, 2026
0d8fddc
fix: use POSIX sh instead of bash for better portability
zerosnacks Jan 21, 2026
20e9926
refactor: simplify tests by removing temp file approach
zerosnacks Jan 21, 2026
02dd09a
fix: add shellcheck directives and has_local workaround for POSIX com…
zerosnacks Jan 21, 2026
76467c2
refactor: simplify test script execution pattern
zerosnacks Jan 21, 2026
e07c1ad
fix: use temp file instead of -c flag for Windows compatibility
zerosnacks Jan 21, 2026
7d8b937
fix: use temp file for all shell script tests on Windows
zerosnacks Jan 21, 2026
428b1ec
chore: release v0.0.3
zerosnacks Jan 21, 2026
1b96d9b
test: add attestation verification tests for installer script
zerosnacks Jan 21, 2026
56ebc83
fmt
zerosnacks Jan 21, 2026
27658ec
fix: use printf instead of echo -n for POSIX compatibility
zerosnacks Jan 21, 2026
5ced3b9
fix: add .exe extension for Windows binaries
zerosnacks Jan 21, 2026
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
3 changes: 3 additions & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Ensure consistent line endings across platforms
*.rs text eol=lf
*.sh text eol=lf
24 changes: 24 additions & 0 deletions .github/scripts/shellcheck.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
#!/usr/bin/env bash

# runs shellcheck and prints GitHub Actions annotations for each warning and error
# https://github.com/koalaman/shellcheck

IGNORE_DIRS=(
"./.git/*"
"./target/*"
)

ignore_args=()
for dir in "${IGNORE_DIRS[@]}"; do
ignore_args+=(-not -path "$dir")
done

find . -name "*.sh" "${ignore_args[@]}" -exec shellcheck -f gcc {} + | \
while IFS=: read -r file line col severity msg; do
level="warning"
[[ "$severity" == *error* ]] && level="error"
file="${file#./}"
echo "::${level} file=${file},line=${line},col=${col}::${file}:${line}:${col}:${msg}"
done

exit "${PIPESTATUS[0]}"
30 changes: 22 additions & 8 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,18 +30,18 @@ jobs:
target: x86_64-unknown-linux-gnu
platform: linux
arch: amd64
# - os: depot-ubuntu-22.04-arm
# target: aarch64-unknown-linux-gnu
# platform: linux
# arch: arm64
- os: depot-ubuntu-22.04-arm
target: aarch64-unknown-linux-gnu
platform: linux
arch: arm64
- os: depot-ubuntu-22.04
target: x86_64-unknown-linux-musl
platform: alpine
arch: amd64
# - os: depot-ubuntu-22.04-arm
# target: aarch64-unknown-linux-musl
# platform: alpine
# arch: arm64
- os: depot-ubuntu-22.04-arm
target: aarch64-unknown-linux-musl
platform: alpine
arch: arm64
- os: macos-14
target: x86_64-apple-darwin
platform: darwin
Expand Down Expand Up @@ -89,6 +89,19 @@ jobs:
persist-credentials: false
- uses: crate-ci/typos@65120634e79d8374d1aa2f27e54baa0c364fff5a # v1

shellcheck:
runs-on: depot-ubuntu-latest
timeout-minutes: 5
permissions:
contents: read
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- name: Shellcheck
shell: bash
run: ./.github/scripts/shellcheck.sh

clippy:
runs-on: ubuntu-latest
timeout-minutes: 30
Expand Down Expand Up @@ -131,6 +144,7 @@ jobs:
needs:
- test
- typos
- shellcheck
- clippy
- rustfmt
timeout-minutes: 30
Expand Down
31 changes: 29 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
name: Release

permissions: {}

on:
push:
tags:
Expand All @@ -10,6 +12,10 @@ env:

jobs:
build:
permissions:
id-token: write
contents: read
attestations: write
runs-on: ${{ matrix.os }}
timeout-minutes: 30
strategy:
Expand Down Expand Up @@ -74,21 +80,42 @@ jobs:
run: cargo build --profile dist --target ${{ matrix.target }}

- name: Prepare artifact
id: artifacts
shell: bash
run: |
if [[ "${{ matrix.platform }}" == "win32" ]]; then
exe=".exe"
fi
cp target/${{ matrix.target }}/dist/foundryup${exe} foundryup_${{ matrix.platform }}_${{ matrix.arch }}
bin_name="foundryup_${{ matrix.platform }}_${{ matrix.arch }}${exe}"
cp target/${{ matrix.target }}/dist/foundryup${exe} "$bin_name"
printf 'bin_name=%s\n' "$bin_name" >> "$GITHUB_OUTPUT"
printf 'attestation_file=%s\n' "foundryup_${{ matrix.platform }}_${{ matrix.arch }}.attestation.txt" >> "$GITHUB_OUTPUT"

- name: Generate attestation
id: attestation
uses: actions/attest-build-provenance@v3
with:
subject-path: ${{ steps.artifacts.outputs.bin_name }}

- name: Record attestation URL
env:
ATTESTATION_URL: ${{ steps.attestation.outputs.attestation-url }}
ATTESTATION_FILE: ${{ steps.artifacts.outputs.attestation_file }}
shell: bash
run: printf '%s\n' "$ATTESTATION_URL" > "$ATTESTATION_FILE"

- name: Upload artifact
uses: actions/upload-artifact@v6
with:
name: foundryup_${{ matrix.platform }}_${{ matrix.arch }}
path: foundryup_${{ matrix.platform }}_${{ matrix.arch }}
path: |
${{ steps.artifacts.outputs.bin_name }}
${{ steps.artifacts.outputs.attestation_file }}
if-no-files-found: error

release:
permissions:
contents: write
runs-on: ubuntu-latest
needs: build
timeout-minutes: 30
Expand Down
2 changes: 1 addition & 1 deletion Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[package]
name = "foundryup"
version = "2.0.0"
version = "0.0.3"
edition = "2024"
rust-version = "1.85"
license = "MIT OR Apache-2.0"
Expand Down
113 changes: 104 additions & 9 deletions foundryup-init.sh
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ has_local() {

has_local 2>/dev/null || alias local=typeset

set -u
set -eu

FOUNDRYUP_REPO="foundry-rs/foundryup"
BASE_DIR="${XDG_CONFIG_HOME:-$HOME}"
FOUNDRY_DIR="${FOUNDRY_DIR:-$BASE_DIR/.foundry}"
FOUNDRYUP_BIN_DIR="$FOUNDRY_DIR/bin"
FOUNDRYUP_IGNORE_VERIFICATION="${FOUNDRYUP_IGNORE_VERIFICATION:-false}"

usage() {
cat <<EOF
Expand All @@ -36,13 +37,15 @@ Options:
-v, --verbose Enable verbose output
-q, --quiet Disable progress output
-y, --yes Skip confirmation prompt
-f, --force Skip attestation verification (INSECURE)
-h, --help Print help
-V, --version Print version

All other options are passed to foundryup after installation.

Environment variables:
FOUNDRYUP_VERSION Install a specific version of foundryup
FOUNDRYUP_VERSION Install a specific version of foundryup
FOUNDRYUP_IGNORE_VERIFICATION Skip attestation verification if set to "true"
EOF
}

Expand Down Expand Up @@ -82,6 +85,9 @@ main() {
_quiet=yes
_passthrough_args="$_passthrough_args $arg"
;;
-f|--force)
FOUNDRYUP_IGNORE_VERIFICATION=true
;;
-y|--yes)
_need_tty=no
_passthrough_args="$_passthrough_args $arg"
Expand All @@ -93,12 +99,20 @@ main() {
done

local _url
local _attestation_url
local _base_url
local _ext
_ext=$(get_ext "$_arch")
if [ "${FOUNDRYUP_VERSION+set}" = 'set' ]; then
say "installing foundryup version $FOUNDRYUP_VERSION"
_url="https://github.com/${FOUNDRYUP_REPO}/releases/download/v${FOUNDRYUP_VERSION}/foundryup_${_arch}"
_base_url="https://github.com/${FOUNDRYUP_REPO}/releases/download/v${FOUNDRYUP_VERSION}"
_url="${_base_url}/foundryup_${_arch}${_ext}"
_attestation_url="${_base_url}/foundryup_${_arch}.attestation.txt"
else
say "installing latest foundryup"
_url="https://github.com/${FOUNDRYUP_REPO}/releases/latest/download/foundryup_${_arch}"
_base_url="https://github.com/${FOUNDRYUP_REPO}/releases/latest/download"
_url="${_base_url}/foundryup_${_arch}${_ext}"
_attestation_url="${_base_url}/foundryup_${_arch}.attestation.txt"
fi

if [ "$_verbose" = "yes" ]; then
Expand All @@ -111,17 +125,72 @@ main() {
exit 1
fi
local _file="${_dir}/foundryup"
local _attestation_file="${_dir}/attestation.txt"
local _expected_hash=""

# Download attestation and extract expected hash (unless skipping verification)
if [ "$FOUNDRYUP_IGNORE_VERIFICATION" = "true" ]; then
say "skipping attestation verification (--force or FOUNDRYUP_IGNORE_VERIFICATION set)"
else
say "downloading attestation..."
# Use curl/wget directly to avoid the downloader's exit-on-404 behavior
if try_download "$_attestation_url" "$_attestation_file"; then
local _attestation_artifact_link
_attestation_artifact_link="$(head -n1 "$_attestation_file" | tr -d '\r')"

if [ -n "$_attestation_artifact_link" ] && ! grep -q 'Not Found' "$_attestation_file"; then
say "verifying attestation..."
local _sigstore_file="${_dir}/attestation.sigstore.json"

if try_download "${_attestation_artifact_link}/download" "$_sigstore_file"; then
# Extract the payload from the sigstore JSON and decode it
local _payload_b64
local _payload_json
_payload_b64=$(awk '/"payload":/ {gsub(/[",]/, "", $2); print $2; exit}' "$_sigstore_file")
_payload_json=$(printf '%s' "$_payload_b64" | base64 -d 2>/dev/null || printf '%s' "$_payload_b64" | base64 -D 2>/dev/null || true)

if [ -n "$_payload_json" ]; then
# Extract SHA256 hash from the payload
_expected_hash=$(printf '%s' "$_payload_json" | grep -oE '"sha256"[[:space:]]*:[[:space:]]*"[a-fA-F0-9]{64}"' | head -1 | grep -oE '[a-fA-F0-9]{64}')
fi

rm -f "$_sigstore_file"
fi
fi

rm -f "$_attestation_file"
fi

if [ -z "$_expected_hash" ]; then
warn "no attestation found for this release, skipping verification"
fi
fi

say "downloading foundryup..."

ensure mkdir -p "$_dir"
ensure downloader "$_url" "$_file" "$_arch"

# Verify the downloaded binary against the attestation hash
if [ -n "$_expected_hash" ]; then
say "verifying binary integrity..."
local _actual_hash
_actual_hash=$(compute_sha256 "$_file")

if [ "$_actual_hash" != "$_expected_hash" ]; then
err "hash verification failed:
expected: $_expected_hash
actual: $_actual_hash
Use --force to skip verification (INSECURE)"
fi
say "binary verified ✓"
fi

ensure chmod u+x "$_file"

if [ ! -x "$_file" ]; then
err "cannot execute $_file (likely because of mounting /tmp as noexec)."
err "please copy the file to a location where you can execute binaries and run ./foundryup"
exit 1
err "cannot execute $_file (likely because of mounting /tmp as noexec).
please copy the file to a location where you can execute binaries and run ./foundryup"
fi

say "installing foundryup to $FOUNDRYUP_BIN_DIR..."
Expand Down Expand Up @@ -179,7 +248,6 @@ get_architecture() {
;;
*)
err "unsupported OS: $_ostype"
exit 1
;;
esac

Expand All @@ -197,13 +265,19 @@ get_architecture() {
;;
*)
err "unsupported architecture: $_cputype"
exit 1
;;
esac

RETVAL="${_ostype}_${_cputype}"
}

get_ext() {
case "$1" in
win32_*) echo ".exe" ;;
*) echo "" ;;
esac
}

is_musl() {
if [ -f /etc/os-release ]; then
grep -qi "alpine" /etc/os-release 2>/dev/null
Expand Down Expand Up @@ -251,6 +325,27 @@ assert_nz() {
fi
}

compute_sha256() {
if check_cmd sha256sum; then
sha256sum "$1" | cut -d' ' -f1 | sed 's/^\\//'
elif check_cmd shasum; then
shasum -a 256 "$1" | cut -d' ' -f1
else
err "need 'sha256sum' or 'shasum' for verification"
fi
}

# Download without exiting on failure (used for optional files like attestations)
try_download() {
if check_cmd curl; then
curl --proto '=https' --tlsv1.2 --silent --fail --location "$1" --output "$2" 2>/dev/null
elif check_cmd wget; then
wget --https-only --secure-protocol=TLSv1_2 -q "$1" -O "$2" 2>/dev/null
else
return 1
fi
}

ensure() {
if ! "$@"; then
err "command failed: $*"
Expand Down
Loading