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
1 change: 1 addition & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Since this is a library build in native go, the files are mostly organized follo
- LICENSE is the license file for the project.
- README.md provides an overview of the project, installation instructions, usage examples, and other relevant information.
- go.mod and go.sum manage the project's dependencies.
- internal/selfupdate/ implements `machineid update`. It is CLI-only: the root library never imports it, makes no network requests and gains no dependencies from it.
- \*.go files contain the main source code of the library.
- \*\_test.go files contain the test cases for the library.

Expand Down
12 changes: 12 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -233,6 +233,18 @@ jobs:
codesign --verify --verbose=4 "./dist/macos/${bin}"
done

# The universal zip carries the same signed binary as the .pkg. It is
# what `machineid update` installs when the running binary does not
# live in /usr/local/bin (for example a `go install` copy).
- name: Create universal zip archives
run: |
mkdir -p ./dist/assets
for bin in machineid; do
zip --junk-paths "./dist/assets/${bin}-darwin-universal.zip" "./dist/macos/${bin}"
shasum -a 256 "./dist/assets/${bin}-darwin-universal.zip" \
| cut -d ' ' -f 1 > "./dist/assets/${bin}-darwin-universal.zip.sha256"
done

- name: Create, sign, notarize & staple .pkg installers
env:
MACOS_INSTALLER_SIGNING_IDENTITY: ${{ secrets.MACOS_INSTALLER_SIGNING_IDENTITY }}
Expand Down
14 changes: 14 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,20 @@ Thank you for your interest in contributing to machineid! This document provides

Runnable examples are listed with `go doc -ex github.com/slashdevops/machineid`.

### Release asset names are a contract

`machineid update` downloads assets by name from the GitHub release. The names are pinned in
`internal/selfupdate/asset_test.go`; renaming an asset in the Makefile or the release workflow
fails that test on purpose. Today's names:

| Platform | Asset | Checksum | Signature |
|----------|-------|----------|-----------|
| Linux amd64/arm64 | `machineid-linux-<arch>.zip` (contains `machineid`) | `machineid-linux-<arch>.sha256` | `machineid-linux-<arch>.sigstore.json` |
| macOS | `machineid-darwin-universal.pkg` | `machineid-darwin-universal.sha256` | Apple Developer ID, notarized |
| macOS (in-place updates) | `machineid-darwin-universal.zip` (contains `machineid`) | `machineid-darwin-universal.zip.sha256` | Apple codesign on the binary |

If a name has to change, change the test and the updater together and note it in the release.

### Getting Started

1. Fork the repository on GitHub
Expand Down
61 changes: 59 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ id, err := machineid.New().WithCPU().WithSystemUUID().ID(ctx)

- [✨ Features](#-features)
- [📦 Installation](#-installation)
- [⬆️ Updating the CLI](#️-updating-the-cli)
- [🚀 Quick start](#-quick-start)
- [🖥️ CLI](#️-cli)
- [📖 Library guide](#-library-guide)
Expand Down Expand Up @@ -115,7 +116,7 @@ unzip machineid.zip && sudo install -m 0755 machineid /usr/local/bin/machineid

**🪟 Windows**: use `go install` above or build from source. Pre-built Windows binaries are not published yet.

How to verify a download: [macOS signing and notarization](docs/macos-signing.md) and [Linux Sigstore verification](docs/linux-signing.md).
How to verify a download: [macOS signing and notarization](docs/macos-signing.md) and [Linux Sigstore verification](docs/linux-signing.md). Already installed? See [Updating the CLI](#️-updating-the-cli).

#### From source

Expand All @@ -128,6 +129,61 @@ make build

---

## ⬆️ Updating the CLI

Once installed, the CLI updates itself. Full guide with per-platform details, script recipes and a troubleshooting table: **[docs/updating.md](docs/updating.md)**.

```bash
machineid update # check, show the plan, ask, install
machineid update -check # what would happen, nothing changes
machineid update -yes # non-interactive (sudo on macOS for the .pkg)
```

It looks up the newest release, shows a checklist and the plan, asks for confirmation, downloads the asset for your platform, verifies its SHA-256 and signature, and replaces the binary you are running. Nothing is downloaded until every check has passed. Root is never requested; where it is needed (the macOS package) the exact `sudo` command is printed.

```text
$ machineid update
Checking for updates…
✓ current version v0.2.0
✓ running binary /usr/local/bin/machineid
✓ platform supported darwin/arm64
✓ latest release v0.3.0 (live, 4 of 5 checks left this hour)
✓ install target /usr/local/bin (running as root)

→ Updating machineid v0.2.0 → v0.3.0 using the signed macOS package

Update machineid now? [y/N] y
downloading machineid-darwin-universal.pkg…
✓ SHA-256 verified
✓ pkgutil: Developer ID Installer: SlashDevOps
✓ installed to /usr/local/bin

✅ Updated to v0.3.0
```

| Flag | Meaning |
|------|---------|
| `-check` | Report what would happen and change nothing. Served from the cache when it is under an hour old. |
| `-refresh` | Look up the latest release now instead of using the cache. |
| `-version TAG` | Install a specific release, e.g. `-version v0.2.0`. This is also how to go back a version. |
| `-method auto\|release\|go` | `release` installs the signed asset (default where one exists), `go` rebuilds with `go install`. |
| `-force` | Install to the method's location even if this binary lives elsewhere, and reinstall an equal version. |
| `-yes` | Do not ask for confirmation. Required when stdin is not a terminal. |
| `-require-signature` | Fail unless the signature was verified: Sigstore via `cosign` on Linux, Apple's on macOS. Without the flag a missing `cosign` only prints a warning. |

| Exit code | Meaning |
|-----------|---------|
| `0` | Updated, already current, `-check`, or you declined |
| `1` | A prerequisite was not met. **Nothing was attempted.** Fix it and re-run. |
| `2` | Invalid arguments |
| `3` | The update was attempted and failed. A checksum failure leaves the old binary untouched. |

**Where it installs.** The Linux zip and the macOS universal zip replace the binary in place, wherever it is. The macOS `.pkg` always installs to `/usr/local/bin` and needs root, so it is only chosen when that is where you are running from. `go install` always writes to `GOBIN`. If the chosen method would land somewhere else, the update refuses and tells you which flag targets your copy. Windows has no published binaries yet, so it uses `-method go`.

**Network use and limits.** `machineid update` is the **only** thing in this tool that touches the network. Normal runs, `-validate` and `-version` never do, and there is no background check. The lookup asks `github.com` for the newest tag with a plain HTTPS request, without the GitHub API and without any token. The answer is cached for an hour and at most **5 live lookups per hour** are made per user, so a cron job running `machineid update -check` costs GitHub nothing after the first call. `MACHINEID_UPDATE_BUDGET` raises the limit if you must. Downloads only happen after you confirm a newer version.

---

## 🚀 Quick start

```go
Expand Down Expand Up @@ -451,7 +507,8 @@ Be deliberate about which of these your users are likely to do.
- 🪪 The output contains no personally identifiable information.
- ⏱️ Every system command has a timeout and is killed, together with its output pipes, when the context ends.
- 🛡️ Firmware sentinels (nil and max UUIDs, "To be filled by O.E.M.") are rejected so they can never make two different machines share an ID.
- 🔏 Release binaries are signed: Apple Developer ID plus notarization on macOS, Sigstore keyless signatures on Linux.
- 🔏 Release binaries are signed: Apple Developer ID plus notarization on macOS, Sigstore keyless signatures on Linux. `machineid update` verifies both before installing.
- 📴 The tool never makes a network request unless you run `machineid update`. There is no telemetry and no background update check.

Please report vulnerabilities as described in [SECURITY.md](SECURITY.md).

Expand Down
16 changes: 14 additions & 2 deletions cmd/machineid/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,18 @@
//
// Exit codes: 0 success, 1 generation or validation failed, 2 invalid
// arguments. Run machineid -h for the full flag list.
//
// The update verb, machineid update, replaces this binary with the latest
// GitHub release. It is the only part of the program that uses the network
// and it never runs unless asked. See machineid update -h.
package main

import (
"context"
"encoding/json"
"flag"
"fmt"
"io"
"log/slog"
"os"
"os/signal"
Expand All @@ -34,6 +39,12 @@ import (
const applicationName = "machineid"

func main() {
// `machineid update` is a verb, not a flag: it has its own flag set and
// is the only code path in this program that touches the network.
if isUpdateVerb(os.Args[1:]) {
os.Exit(runUpdate(os.Args[2:], os.Stdin, os.Stdout, os.Stderr))
}

// Hardware component flags
cpu := flag.Bool("cpu", false, "Include CPU identifier")
motherboard := flag.Bool("motherboard", false, "Include motherboard serial number")
Expand Down Expand Up @@ -184,7 +195,8 @@ func printUsage() {
fmt.Fprintf(w, "Usage:\n")
fmt.Fprintf(w, " %s [-cpu] [-uuid] [-motherboard] [-mac] [-disk] [options]\n", applicationName)
fmt.Fprintf(w, " %s -all [options]\n", applicationName)
fmt.Fprintf(w, " %s -vm [options]\n\n", applicationName)
fmt.Fprintf(w, " %s -vm [options]\n", applicationName)
fmt.Fprintf(w, " %s update [options] Update this binary to the latest release (see: %s update -h)\n\n", applicationName, applicationName)

fmt.Fprintf(w, "When no component flags are specified, the default is -cpu -motherboard -uuid.\n\n")

Expand Down Expand Up @@ -238,7 +250,7 @@ func printUsage() {
fmt.Fprintf(w, " 2 Invalid arguments\n")
}

func printFlag(w *os.File, name, desc string) {
func printFlag(w io.Writer, name, desc string) {
fmt.Fprintf(w, " %-20s %s\n", name, desc)
}

Expand Down
Loading
Loading