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
54 changes: 54 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ jobs:
run: |
actionlint
zizmor --persona regular .github/workflows
- name: Check the install script
# Served at https://docs.configfile.sh/install.sh, run by any POSIX shell.
run: shellcheck --shell=sh docs/public/install.sh

test:
strategy:
Expand Down Expand Up @@ -82,6 +85,57 @@ jobs:
configfile --help
HOME="$RUNNER_TEMP/home" configfile history

binary:
# Builds the standalone binary on each system it is made for, as
# publish.yml does for a release, and runs the end-to-end tests with it.
strategy:
fail-fast: false
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
with:
version: 2026.9.18 # mise version, updated by Renovate
# The CLI only: the documentation site (docs/) is built by the docs job.
- run: pnpm install --frozen-lockfile --filter configfile
- run: pnpm build:binary
- name: Install the binary with the install script
# As users get it: from a release laid out as on GitHub, with SHA256SUMS.
run: |
archive=$(ls release/configfile-*.tar.gz)
name=$(basename "$archive")
tar -tzf "$archive" | sort > "$RUNNER_TEMP/members"
printf '%s\n' ./ ./LICENSE ./THIRD-PARTY-LICENSES ./configfile | sort |
diff - "$RUNNER_TEMP/members"
release="$RUNNER_TEMP/releases/download/ci"
mkdir -p "$release"
cp "$archive" "$release/"
(cd "$release" && shasum -a 256 "$name" > SHA256SUMS)
CONFIGFILE_DOWNLOAD_URL="file://$RUNNER_TEMP/releases" CONFIGFILE_VERSION=ci \
CONFIGFILE_INSTALL_DIR="$RUNNER_TEMP/bin" sh docs/public/install.sh
- name: Run the end-to-end tests with the installed binary
run: CONFIGFILE_BINARY="$RUNNER_TEMP/bin/configfile" pnpm vitest run tests/e2e.test.ts
- name: Answer a question on a terminal, whatever NODE_OPTIONS says
run: |
bin="$RUNNER_TEMP/bin/configfile"
export HOME="$RUNNER_TEMP/home" NODE_OPTIONS="--require /nonexistent.js"
mkdir -p "$HOME"
remote="$RUNNER_TEMP/remote"
git init --quiet "$remote"
git -C "$remote" -c user.name=ci -c user.email=ci@example.com commit --quiet --allow-empty -m first
# The URL is typed once the question is shown; the input stays open meanwhile.
answer() { sleep 3; printf '%s\r' "$remote"; sleep 10; }
if [ "$RUNNER_OS" = macOS ]; then
answer | script -q /dev/null "$bin" init
else
answer | script -qec "$bin init" /dev/null
fi
grep -F "$remote" "$HOME/.configfilerc"

docs:
# Builds the documentation site (docs/), as docs-deploy.yml publishes it.
runs-on: ubuntu-latest
Expand Down
98 changes: 82 additions & 16 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,16 +4,23 @@ name: Publish
# master, the release branch of git-flow:
#
# - build: checks the tag, runs the checks and packs the package once;
# - github-release: publishes that tarball, its SHA256SUMS and the CHANGELOG
# section as a GitHub release, with a build provenance attestation
# (`gh attestation verify configfile-<version>.tgz --repo mindsers/configfile`);
# - binaries: builds the standalone binary on each system it is made for
# (tools/build-binary.ts), and runs the end-to-end tests with it;
# - github-release: publishes that tarball, the binaries, their SHA256SUMS
# and the CHANGELOG section as a GitHub release, with a build provenance
# attestation for each archive (`gh attestation verify <archive> --repo
# mindsers/configfile`).
# The binaries keep the same names from one release to the next, so that
# docs/public/install.sh finds the latest one at releases/latest/download/;
# - npm: publishes the same tarball to npm, with npm trusted publishing (no
# token stored, provenance statement). It only runs when the repository
# variable NPM_PUBLISH is "true", once trusted publishing is set up on
# npmjs.com (package settings > Trusted publishing): repository
# mindsers/configfile, workflow publish.yml, environment npm. To publish a
# version released while it was off, set the variable and re-run this
# workflow for its tag: an existing GitHub release keeps its notes.
# workflow for its tag: an existing GitHub release keeps its notes and the
# files it already has (a rebuilt archive is not byte for byte the same, and
# would break the checksums users have); only missing files are added.

on:
push:
Expand Down Expand Up @@ -53,11 +60,8 @@ jobs:
run: |
mkdir release
npm pack --pack-destination release
cd release
[ -f "configfile-$TAG.tgz" ] || {
[ -f "release/configfile-$TAG.tgz" ] || {
echo "::error::npm pack did not produce configfile-$TAG.tgz."; exit 1; }
sha256sum "configfile-$TAG.tgz" > SHA256SUMS
cat SHA256SUMS
- name: Extract the release notes from the CHANGELOG
env:
TAG: ${{ github.ref_name }}
Expand All @@ -75,8 +79,36 @@ jobs:
path: release/
if-no-files-found: error

github-release:
binaries:
needs: build
strategy:
matrix:
os: [ubuntu-24.04, ubuntu-24.04-arm, macos-15, macos-15-intel]
runs-on: ${{ matrix.os }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false
# No cache: nothing restored from another run can end up in a release.
- uses: jdx/mise-action@7a4e45a543138629540c9a1616d08632b893e492 # v5.0.1
with:
version: 2026.9.18 # mise version, updated by Renovate
cache: false
- run: pnpm install --frozen-lockfile --filter configfile
- run: pnpm build:binary
- name: Run the end-to-end tests with the binary
run: |
mkdir "$RUNNER_TEMP/binary"
tar -xzf release/configfile-*.tar.gz -C "$RUNNER_TEMP/binary"
CONFIGFILE_BINARY="$RUNNER_TEMP/binary/configfile" pnpm vitest run tests/e2e.test.ts
- uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: binary-${{ matrix.os }}
path: release/configfile-*.tar.gz
if-no-files-found: error

github-release:
needs: [build, binaries]
runs-on: ubuntu-latest
permissions:
contents: write # create the release
Expand All @@ -86,23 +118,57 @@ jobs:
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: release
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: binary-*
merge-multiple: true
- name: Keep the files of an existing release
id: keep
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
gh release view "$TAG" --repo "$GITHUB_REPOSITORY" --json assets \
--jq '.assets[].name' > published.txt 2> /dev/null || : > published.txt
# Kept aside until attested: only what this run built is attested.
mkdir published
grep '^configfile-' published.txt | while read -r name; do
rm -f "$name"
gh release download "$TAG" --repo "$GITHUB_REPOSITORY" --pattern "$name" --dir published
echo "::notice::$name is kept as published."
done
if ls configfile-* > /dev/null 2>&1; then
echo "built=true" >> "$GITHUB_OUTPUT"
fi
# Attests the files built by this run only: a published file, which may
# have been uploaded by hand, keeps the attestation it has (if any).
- uses: actions/attest-build-provenance@4d101475d8b20a2381f78447822ac1eab6504dd8 # v4.2.2
if: steps.keep.outputs.built == 'true'
with:
subject-path: configfile-${{ github.ref_name }}.tgz
subject-path: configfile-*
- name: Compute the checksums
run: |
find published -type f -exec mv {} . \;
sha256sum configfile-* > SHA256SUMS
cat SHA256SUMS
- name: Create the GitHub release
env:
GH_TOKEN: ${{ github.token }}
TAG: ${{ github.ref_name }}
run: |
# A release created by hand, or by an earlier run: its notes are kept,
# and the files attested above replace any older ones.
# A release created by hand, or by an earlier run: its notes and files
# are kept, the missing files added, and SHA256SUMS (which covers the
# files as published) replaced.
if gh release view "$TAG" --repo "$GITHUB_REPOSITORY" > /dev/null 2>&1; then
gh release upload "$TAG" "configfile-$TAG.tgz" SHA256SUMS \
--repo "$GITHUB_REPOSITORY" --clobber
echo "::notice::The $TAG release already existed: its notes are kept, its files replaced."
for file in configfile-*; do
grep -qxF "$file" published.txt ||
gh release upload "$TAG" "$file" --repo "$GITHUB_REPOSITORY"
done
gh release upload "$TAG" SHA256SUMS --repo "$GITHUB_REPOSITORY" --clobber
echo "::notice::The $TAG release already existed: its notes and files are kept, missing files added."
exit 0
fi
gh release create "$TAG" "configfile-$TAG.tgz" SHA256SUMS \
gh release create "$TAG" configfile-* SHA256SUMS \
--repo "$GITHUB_REPOSITORY" --verify-tag \
--title "$TAG" --notes-file notes.md

Expand Down
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -66,3 +66,4 @@ dist/

# Astro (docs/)
.astro/
release/
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]
### Added
- Standalone binaries for macOS (13.5 or later) and Linux (GNU C library 2.28 or later), on x64
and arm64, with each release, and an install script:
`curl -fsSL https://docs.configfile.sh/install.sh | sh` installs configfile on a new machine
without Node.js or a package manager ([#301](https://github.com/mindsers/configfile/issues/301)).
- Scripts get the repository folder, their name and the system in the `CONFIGFILE_REPO`,
`CONFIGFILE_SCRIPT` and `CONFIGFILE_OS` environment variables
([#295](https://github.com/mindsers/configfile/issues/295)).
Expand Down
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ configfile update # later: brings the chan

## Installation

With one command, on macOS or Linux (no package manager or Node.js needed):

```bash
curl -fsSL https://docs.configfile.sh/install.sh | sh
```

With [Homebrew](https://brew.sh), on macOS or Linux (Node.js and git come with it):

```bash
Expand Down
130 changes: 130 additions & 0 deletions docs/public/install.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
#!/bin/sh
# Installs configfile on macOS or Linux, without Node.js or a package manager:
#
# curl -fsSL https://docs.configfile.sh/install.sh | sh
#
# It downloads the standalone binary for this system from the GitHub release,
# checks it against the release's SHA256SUMS, and installs it as
# ~/.local/bin/configfile. Run it again to update configfile.
#
# Settings, as environment variables (put them before `sh`):
# CONFIGFILE_VERSION the version to install (default: the latest release;
# releases have binaries from the one that
# introduced them, 1.0.0 has none)
# CONFIGFILE_INSTALL_DIR where to install it (default: ~/.local/bin)
# CONFIGFILE_DOWNLOAD_URL where releases are downloaded from (default: the
# GitHub releases of mindsers/configfile; for tests
# and mirrors)
#
# Source: https://github.com/mindsers/configfile/blob/master/docs/public/install.sh

set -eu

releases="${CONFIGFILE_DOWNLOAD_URL:-https://github.com/mindsers/configfile/releases}"

say() { printf '%s\n' "$*"; }
fail() {
printf 'configfile installer: %s\n' "$*" >&2
exit 1
}

# The name of the archive for this system, such as configfile-macos-arm64.tar.gz.
archive_name() {
case "$(uname -s)" in
Darwin) system=macos ;;
Linux) system=linux ;;
*) fail "no configfile binary for $(uname -s): it runs on macOS and Linux." ;;
esac
case "$(uname -m)" in
x86_64 | amd64) arch=x64 ;;
arm64 | aarch64) arch=arm64 ;;
*) fail "no configfile binary for $(uname -m) processors: install it with npm instead (https://docs.configfile.sh/getting-started/installation/)." ;;
esac
# A shell running under Rosetta reports x86_64 on Apple silicon.
if [ "$system" = macos ] && [ "$arch" = x64 ] &&
[ "$(sysctl -n sysctl.proc_translated 2>/dev/null || true)" = 1 ]; then
arch=arm64
fi
# The binaries are built for the GNU C library, which Alpine doesn't use.
if [ "$system" = linux ] && ldd --version 2>&1 | grep -qi musl; then
fail "the configfile binary needs the GNU C library, not musl (Alpine): install it with npm instead (https://docs.configfile.sh/getting-started/installation/)."
fi
say "configfile-$system-$arch.tar.gz"
}

download() { # url file
if command -v curl >/dev/null 2>&1; then
curl -fsSL --retry 2 -o "$2" "$1" || fail "cannot download $1."
elif command -v wget >/dev/null 2>&1; then
wget -q -O "$2" "$1" || fail "cannot download $1."
else
fail "curl or wget is needed to download configfile."
fi
}

sha256() { # file
if command -v sha256sum >/dev/null 2>&1; then
sha256sum "$1" | cut -d ' ' -f 1
elif command -v shasum >/dev/null 2>&1; then
shasum -a 256 "$1" | cut -d ' ' -f 1
else
fail "sha256sum or shasum is needed to check the download."
fi
}

main() {
name="$(archive_name)"
version="${CONFIGFILE_VERSION:-latest}"
if [ "$version" = latest ]; then
base="$releases/latest/download"
else
base="$releases/download/$version"
fi
dir="${CONFIGFILE_INSTALL_DIR:-$HOME/.local/bin}"

work="$(mktemp -d)"
draft="$dir/.configfile.$$"
trap 'rm -rf "$work"; rm -f "$draft"' EXIT
# Stopped (Ctrl+C, kill): ends here, the exit trap cleaning up.
trap 'exit 130' INT
trap 'exit 143' TERM

say "Downloading $base/$name..."
download "$base/$name" "$work/$name"
download "$base/SHA256SUMS" "$work/SHA256SUMS"
expected="$(awk -v name="$name" '$2 == name || $2 == "*" name { print $1 }' "$work/SHA256SUMS")"
[ -n "$expected" ] || fail "SHA256SUMS of the release has no line for $name."
actual="$(sha256 "$work/$name")"
[ "$actual" = "$expected" ] ||
fail "$name does not match the checksum of the release: it was not installed."

tar -xzf "$work/$name" -C "$work" ./configfile || fail "cannot extract $name."
mkdir -p "$dir" || fail "cannot create $dir."
{ cp "$work/configfile" "$draft" && chmod 755 "$draft"; } || fail "cannot install configfile in $dir."
# Checked before it replaces anything: an installed configfile is kept when
# the new one cannot run on this system.
installed="$("$draft" --version)" ||
fail "the downloaded configfile does not start on this system: nothing was changed in $dir."
# Moved into place in one step, so a configfile running meanwhile is not cut short.
mv -f "$draft" "$dir/configfile" || fail "cannot install configfile in $dir."
say "configfile $installed is installed in $dir/configfile."

case ":$PATH:" in
*":$dir:"*)
found="$(command -v configfile || true)"
if [ -n "$found" ] && [ "$found" != "$dir/configfile" ]; then
say "Another configfile comes first in your PATH: $found. Remove it, or put $dir first."
fi
;;
*) say "$dir is not in your PATH: add it to your shell's profile, for example: export PATH=\"$dir:\$PATH\"" ;;
esac
if ! command -v git >/dev/null 2>&1; then
case "$(uname -s)" in
Darwin) say "configfile needs git: install it with xcode-select --install" ;;
*) say "configfile needs git: install it with your system's package manager." ;;
esac
fi
say 'Next: run "configfile init" to connect it to your dotfiles repository.'
}

main
Loading
Loading