Skip to content

feat(binary): onPost hook — runs after install/update#153

Merged
fentas merged 8 commits into
mainfrom
feat/binary-onpost-hook
May 8, 2026
Merged

feat(binary): onPost hook — runs after install/update#153
fentas merged 8 commits into
mainfrom
feat/binary-onpost-hook

Conversation

@fentas

@fentas fentas commented May 7, 2026

Copy link
Copy Markdown
Owner

Summary

Per-binary post-install/update hook. A shell command that runs after a successful download, with context about what happened.

Usage

CLI:

b install --add github.com/arg-sh/argsh --on-post 'argsh builtin ${B_EVENT}'

b.yaml:

binaries:
  github.com/arg-sh/argsh:
    onPost: argsh builtin ${B_EVENT}

Environment variables

The hook receives:

Variable Description Example
`B_EVENT` `install` or `update` `install`
`B_NAME` Binary name `argsh`
`B_VERSION` Version being installed `v0.6.6`
`B_FILE` Absolute path to the binary `/project/.bin/argsh`

Behaviour

  • Runs in the project root directory
  • Only after successful download (not on skip, not on failure)
  • Non-zero exit → warning on stderr, not a fatal error
  • Skipped on `--dry-run`
  • `--on-post` flag persists to b.yaml when used with `--add`

Use cases

  • Shell completion generation: `kubectl completion bash > .completions/kubectl.bash`
  • Plugin/builtin updates: `argsh builtin update`
  • Post-install validation: `helm version --client`
  • Config scaffolding: `tool init --config .config/tool.yaml`

Test plan

  • `TestRunHook_SetsEnvVars` — all 4 env vars set correctly
  • `TestRunHook_EmptyIsNoOp` — empty string is a no-op
  • `TestRunHook_NonZeroExitReturnsError` — failed hooks return error
  • `TestRunHook_RunsInDir` — hook executes in specified directory
  • `TestBinaryListMarshalYAML_OnPostRoundTrip` — b.yaml round-trip
  • `TestAddToConfig_WithOnPost` — `--on-post` flag persists to config
  • `go test ./...` — all 39 packages pass
  • E2E: `b install --add --on-post 'echo HOOK: ${B_NAME}' github.com/arg-sh/argsh` → hook runs, onPost saved to b.yaml

🤖 Generated with Claude Code

Adds a per-binary post-install/update hook: a shell command that runs
after a successful download, receiving B_EVENT (install|update),
B_NAME, B_VERSION, and B_FILE as env vars. Non-zero exit is surfaced
as a warning, not a fatal error. Skipped when the binary didn't
change.

b.yaml:
  binaries:
    github.com/arg-sh/argsh:
      onPost: argsh builtin ${B_EVENT}

CLI:
  b install --add github.com/arg-sh/argsh --on-post 'argsh builtin ${B_EVENT}'

Implementation:
- LocalBinary gains OnPost (yaml:"onPost") — round-trips via
  BinaryList Marshal/Unmarshal.
- managedKey updated so SaveConfig preserves the field but can also
  remove it when the user deletes it from config.
- RunHook helper in pkg/binary/hook.go — exec.Command with the four
  B_ env vars set, runs in the project root directory.
- install.go / update.go call RunHook after a successful EnsureBinary
  / DownloadBinary, with event="install" or "update" respectively.
- --on-post flag on 'b install' feeds into addToConfig.

Tests:
- TestRunHook_{SetsEnvVars,EmptyIsNoOp,NonZeroExitReturnsError,RunsInDir}
- TestBinaryListMarshalYAML_OnPostRoundTrip
- TestAddToConfig_WithOnPost

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces a per-binary onPost hook that runs a user-provided shell command after binary install/update, and persists that hook via b.yaml/--add so it can be replayed on subsequent runs.

Changes:

  • Add onPost to the binary config model and YAML merge/round-trip behavior.
  • Execute onPost after b install / b update operations.
  • Add unit tests covering hook env vars + YAML/config persistence.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
pkg/state/yamlmerge.go Treat binaries.<name>.onPost as a managed key when rewriting YAML.
pkg/state/types.go Emit onPost during BinaryList.MarshalYAML.
pkg/state/types_test.go Add YAML round-trip test for onPost.
pkg/cli/shared.go Plumb OnPost from config into resolved binary.Binary.
pkg/cli/install.go Add --on-post flag, persist it with --add, and run hook post-install.
pkg/cli/update.go Run hook post-update.
pkg/cli/cli_extra_test.go Test that --on-post persists to config via addToConfig.
pkg/binary/types.go Add OnPost fields to Binary/LocalBinary types and document semantics.
pkg/binary/hook.go Introduce RunHook helper that sets B_* env vars and executes a shell command.
pkg/binary/hook_test.go Add tests for env vars, no-op behavior, non-zero exit, and working directory.

Comment thread pkg/cli/install.go
Comment thread pkg/cli/install.go Outdated
Comment thread pkg/cli/update.go
Comment thread pkg/cli/update.go Outdated
Comment thread pkg/cli/update.go Outdated
Comment thread pkg/binary/types.go Outdated
Comment thread pkg/binary/hook.go Outdated
Comment thread pkg/binary/hook.go
Copilot review round 1:

1. RunHook signature changed from *os.File to io.Writer so callers
   can route through the CLI's IO streams (respecting --quiet,
   output capture). nil defaults to io.Discard. Added docstring
   noting hooks are POSIX-only (sh -c), consistent with existing
   env hooks.

2. install.go: track wasMissing before EnsureBinary(false) so the
   hook only fires when a download actually happened. Force mode
   always counts as downloaded. Streams routed through o.IO.

3. update.go: track 'downloaded' per branch — DownloadBinary
   branches set it on err==nil; EnsureBinary compares pre/post SHA
   to detect whether the file actually changed. Hook gated behind
   '!o.effectiveDryRun()' for --dry-run/--plan-json respect.
   Streams routed through o.IO.

4. Types.go docstring updated to match actual guarantees: only
   fires when on-disk binary changed, not on no-op skip or dry-run.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
macOS /var → /private/var symlink causes t.TempDir() to return the
unresolved path while pwd in the hook resolves it. Use
filepath.EvalSymlinks + strings.TrimSpace for the comparison.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread pkg/binary/hook.go
Comment thread pkg/cli/install.go Outdated
1. RunHook now filters existing B_EVENT/B_NAME/B_VERSION/B_FILE from
   os.Environ() before appending the new values, so platform-dependent
   duplicate-env-key precedence can't make the hook observe a stale
   value from the parent process.

2. addToConfig was persisting o.OnPost (the CLI flag) instead of
   b.OnPost (the per-binary value). Changed to b.OnPost for consistency
   with asset/version/alias which all read from the Binary struct.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread pkg/cli/update.go Outdated
Comment thread pkg/cli/update.go Outdated
1. Only compute pre/post SHA256 in the EnsureBinary branch when
   b.OnPost is non-empty and not dry-run — avoids O(file-size) hashing
   on every update for binaries without hooks.

2. Renamed local 'preSHA' → 'beforeHash' to avoid shadowing the outer
   preSHA map used by refreshLockDigests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread pkg/cli/update.go Outdated
Comment thread pkg/binary/hook.go Outdated
1. EnsureBinary branch now tracks wasMissing before hashing. If the
   binary was deleted from disk, the pre-SHA would fail and downloaded
   would stay false, silently skipping the hook. Now wasMissing=true
   sets downloaded=true after a successful EnsureBinary.

2. Tightened RunHook env-filter comment to say "the four hook-specific
   variables" not "any existing B_ vars".

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread pkg/cli/update.go
Comment thread pkg/cli/update.go
If SHA256File fails before or after EnsureBinary (e.g. transient I/O
error), assume the binary changed and run the hook. Better to run it
unnecessarily than silently skip due to an unrelated file-read error.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 10 out of 10 changed files in this pull request and generated 2 comments.

Comment thread pkg/cli/install.go Outdated
Comment thread pkg/cli/update.go Outdated
…corruption

Hook stdout was routed to o.IO.Out while the progress writer renders
to the same stream concurrently. Any hook output would interleave
with progress bars and corrupt the terminal display.

Route both hook stdout and stderr to o.IO.ErrOut so hook output stays
clean and separate from progress rendering. This is consistent with
how hook warnings are already printed.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@fentas fentas merged commit 8c03360 into main May 8, 2026
8 checks passed
@fentas fentas deleted the feat/binary-onpost-hook branch May 8, 2026 08:43
fentas added a commit that referenced this pull request May 8, 2026
## Summary
Documentation for the `onPost` per-binary hook feature (PR #153).

## Changes
- **docs/b/subcommands/install.mdx** — new "Post-install hooks" section:
`--on-post` flag, env vars table (`B_EVENT`, `B_NAME`, `B_VERSION`,
`B_FILE`), `b.yaml` examples, and behaviour notes (POSIX-only,
non-fatal, stderr output). Added `--on-post` to the flags table.
- **docs/b/subcommands/update.mdx** — "Post-update hooks" section
explaining that `onPost` also fires on update, with a cross-reference to
the install docs.
- **docs/getting-started.mdx** — `onPost` example in the config
walkthrough.
- **docs/glossary.mdx** — `onPost` term definition.
- **README.md** — `--on-post` CLI example in the quick-use block +
`onPost` in the config example section.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
fentas pushed a commit that referenced this pull request May 8, 2026
🤖 I have created a release *beep* *boop*
---


## [4.17.0](v4.16.0...v4.17.0)
(2026-05-08)


### Features

* **binary:** onPost hook — runs after install/update
([#153](#153))
([8c03360](8c03360))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants