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
53 changes: 52 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

## v0.0.50
## v0.0.51

<!-- v0.0.50 was tagged but never published (its release build was red); its
changes ship here under v0.0.51. Tags stay immutable — the v0.0.50 tag is
burned, not re-pointed. -->

### Added

Expand All @@ -14,6 +18,14 @@
(`$e(cmd | exec)`), but refuses a piped stdin — there is no parent left
to feed the replacement.

- **`cmd | line` — the single-line capture reifier.** Reads a one-value
command (`az … --query X -o tsv`, `git rev-parse`, `id -un`) as its one
trimmed line of stdout, as a `string` — replacing the
`$"{$(cmd) |> Seq.exactlyOne}"` mouthful with `let x = cmd | line`. It
joins the reifier family (`complete`/`succeeds`/`exitCode`/`orFail`/`exec`),
raises on a nonzero exit or on 0-or-2+ lines, and composes with the env
sigil (`$e(cmd | line)`) and a value head (`xs | grep foo | line`).

- **`Path.home` and the XDG directory trio (`Path.configHome`,
`Path.stateHome`, `Path.cacheHome`).** The typed stand-in for `~`/`$HOME`,
which never expand in argv — build a path with an interpolation instead:
Expand All @@ -30,6 +42,21 @@
`#history N` shows the last N. Entries render one per line, matching the
history search's display.

- **REPL kill-ring: `Ctrl+W` / `Ctrl+Y`.** `Ctrl+W` kills the previous word,
`Ctrl+Y` yanks (pastes) the last killed text, and `Ctrl+U` / `Ctrl+K`
(kill to line start / end) now feed the same ring. The ring is
session-wide, so a kill on one line yanks into a later one. (Copying from
elsewhere remains your terminal's own mouse selection — there is no
shift-arrow selection, matching every terminal line editor.)

- **`from table` reads `az … -o table` output.** Azure CLI (and other
tabulate-style tools) draw a `------ ----------` dashes rule under the
header; `from table` now skips that separator when it is the first data
row, so both `from table T` and `#infer … from table` work on az output.
kubectl/docker tables (no separator) are unchanged, and a real row that
merely contains a lone `-` cell is kept — only an all-dashes row is
dropped.

- **A checker warning for the `Path.newTempDir` cleanup footgun.** A
`Path.newTempDir ()` binding that is later `Dir.delete`/`Dir.deleteAll`'d
in the same scope now draws an advisory warning pointing at `within tmp`,
Expand All @@ -38,6 +65,18 @@
directory that must outlive its scope, so a binding with no in-scope
delete stays silent. Warning severity — `check` still exits 0.

### Changed

- **A failed command in the REPL is now a quiet exit-code status, not a red
error.** When a bare command you type at the prompt exits nonzero, the
REPL shows a dim `↳ exit N` instead of `error: command failed with exit
code N: …` — the output already streamed and the session continues, so
it reads like a shell's `$?` (and since weir has no `$?`, showing the
code is the only way to see it). The raise itself is unchanged: scripts
still fail-fast, and a command failure inside a value (`$(…)`, a reifier,
a binding) still raises the full error, because there it aborted a
computation.

### Fixed

- **A top-level `if … then … else …` block now parses.** The block form at
Expand All @@ -57,6 +96,18 @@
mixed value types stays a typed record; genuinely uniform data-keyed
objects (a ConfigMap's `data`) are still drafted as an open mapping.

- **Tab-completing a `$name` splice in a command argument now offers session
bindings.** `az … --location $l<TAB>` completes to `$location` (any
binding whose name starts with `l`); before, an argv position offered only
filesystem paths, so a splice never completed. A bare (non-`$`) argument
still completes paths only.

- **`exec` is now refused inside a `plan` block.** A helper that invokes
`cmd | exec` inside a `plan` is refused at runtime, matching the guard on
every other spawn — `execve` does not funnel through the spawn path, so
the guard is applied in `Proc.exec` directly (it could otherwise replace
the process inside a dry run).

## v0.0.49

### Fixed
Expand Down
35 changes: 32 additions & 3 deletions ci/e2e.sh
Original file line number Diff line number Diff line change
Expand Up @@ -452,6 +452,27 @@ WEOF
echo "$out" | grep -qF "cannot take a piped stdin" || fail "value-headed exec must refuse at check"
fi

# ---- the single-line capture reifier [D:reify-line] ------------------
# `cmd | line` reads a one-value CLI's single stdout line as a string —
# the $(cmd) |> Seq.exactlyOne idiom, first-class. Raises on nonzero and
# on 0-or-2+ lines.
lout=$($BIN -e 'let scope = printf "id-abc" | line
echo scope $scope')
expect "| line reifies one stdout line to a string" "scope id-abc" "$lout"
# a trailing newline (the -o tsv shape) is still one line
lout2=$($BIN -e 'let v = sh -c "printf \"val\n\"" | line
echo got $v')
expect "| line handles the trailing newline (one line)" "got val" "$lout2"
# nonzero exit raises
lerr=$($BIN -e 'let x = sh -c "exit 4" | line
print x' 2>&1) && fail "| line must raise on nonzero" || true
echo "$lerr" | grep -qF "exit code 4" || fail "| line lost the nonzero raise: $lerr"
# 2+ lines is the caller's mistake, named
lmulti=$($BIN -e 'let x = sh -c "printf \"a\nb\n\"" | line
print x' 2>&1) && fail "| line must raise on 2+ lines" || true
echo "$lmulti" | grep -qF "expected exactly one line" || fail "| line lost the one-line assert: $lmulti"
echo "e2e ok: | line — one stdout line to a string, nonzero raises, 2+ lines named [D:reify-line]"

# a 2-param generic union checks + evals through the binary (was the
# prelude-Result pin; Result removed [D:no-result], the fixture is now a
# locally-declared Either)
Expand Down Expand Up @@ -1631,7 +1652,7 @@ PYADP
echo "e2e ok: repl cooked-trap (one child run per echo, Enter survives a slow child)"

python3 "$(dirname "$0")/../tests/repl/repl-it-streamed.py" "$BIN" || fail "it FSI-parity / function echo"
echo "e2e ok: it FSI-parity — streamed binds (), misuse teaches the capture, functions echo mini-help [D:repl-it] [D:repl-fn-echo]"
echo "e2e ok: it FSI-parity — streamed binds (), misuse teaches the capture, functions echo mini-help, a failed command quiets to an exit-code status [D:repl-it] [D:repl-fn-echo] [D:repl-cmd-fail]"

python3 "$(dirname "$0")/../tests/repl/repl-directives.py" "$BIN" || fail "repl directives"
echo "e2e ok: repl directives (#help x3, #quit, :q retired, comments no-op, #echo cap)"
Expand Down Expand Up @@ -2448,7 +2469,9 @@ if [ "$IS_WINDOWS" != "1" ] && command -v python3 >/dev/null 2>&1; then
echo "$gzout" | grep -qi "is a terminal\|to a terminal" || fail "gzip refuses its own tty now — the incident's cause removed: $gzout"
rsout=$(printf 'SLEEP 700\nSEND sh -c "read x"\\r\nSLEEP 400\nSEND \\x03\nSLEEP 500\nSEND print (Str.toUpper "revived")\\r\nSLEEP 400\nSEND #quit\\r\n' \
| python3 "$ptyrun" 10 "$BIN" | sed 's/\x1b\[[0-9;?]*[a-zA-Z]//g; s/\x1b[=>]//g')
echo "$rsout" | grep -q "exit code 130" || fail "a REPL ^C must kill the foreground child naming 130: $rsout"
# the child's ^C death renders as the QUIET exit-code status now, not the
# loud error [D:repl-cmd-fail] — still names 130 and the SIGINT note
echo "$rsout" | grep -q "exit 130 (SIGINT" || fail "a REPL ^C must kill the foreground child naming 130: $rsout"
echo "$rsout" | grep -q "REVIVED" || fail "the session must survive its child's ^C: $rsout"
echo "$rsout" | grep -q "^EXIT 0" || fail "the session must end clean after a ^C'd child: $rsout"

Expand Down Expand Up @@ -3934,7 +3957,13 @@ echo "$tto" | grep -qF "the table boundary is read-only" || fail "to table refus
tid=$($BIN -e 'let table = 5
show (table + 1)') || fail "'table' must stay bindable"
echo "$tid" | grep -qF '"6"' || fail "'table' as an identifier: $tid"
echo "e2e ok: from table — header-offset slicing, Wire + Option/<none>, located errors, no to table, 'table' unreserved"
# az `-o table` dashes separator [D:from-table-az]: the rule line under the
# header is skipped, so the real rows read typed — bool/int survive
taz=$($BIN -e 'type Vm = { name: string; running: bool; port: int }
["Name Running Port"; "------ -------- ----"; "web-1 true 8080"; "db-0 false 443"] |> from table Vm |> Seq.where (fun v -> v.running) |> Seq.map (fun v -> v.name) |> Str.join ","')
echo "$taz" | grep -qF "web-1" || fail "az separator skip: real rows must read (got: $taz)"
echo "$taz" | grep -qF -- "------" && fail "az separator must not become a data row: $taz" || true
echo "e2e ok: from table — header-offset slicing, Wire + Option/<none>, located errors, az separator skip, no to table, 'table' unreserved"
rm -rf "$tdir"

# --- #infer from table [D:from-table]: the REPL drafts the row record
Expand Down
7 changes: 7 additions & 0 deletions ci/release-burned.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Burned tags [D:burned-tag-ack]: pushed, never published, never
# re-pointed [D:tag-immutability] — superseded by the next release.
# Swept by ci/release-published.weir: a listed-but-published tag fails
# as stale; an aged-out entry is named for removal without failing.
burned:
- tag: v0.0.50
reason: release run red (release-smoke redaction probe pinned the wrong error path; fixed on main); changes ship in v0.0.51
43 changes: 37 additions & 6 deletions ci/release-published.weir
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,29 @@
// spot that let a publish-wait poll report v0.0.31 shipped while
// /latest still served v0.0.29). A burned tag ages out on its own: the
// next release makes a newer tag the newest, and the burned one is
// never demanded again (the v0.0.30 precedent).
// never demanded again (the v0.0.30 precedent) — but the aging-out
// WINDOW is red and blocks the superseding release's own PR (the check
// eating its repair, the shape [D:release-assets] reversed with
// known-incomplete): ci/release-burned.yaml acknowledges the burn for
// that window [D:burned-tag-ack].

let repo = "weir-shell/weir"

// burned tags [D:burned-tag-ack]: a tag whose release never shipped is
// BURNED — immutable [D:tag-immutability], never re-pointed, superseded
// by the next tag. An entry carries its reason and is SWEPT below: a
// listed tag that IS published fails as stale; an aged-out entry (a
// newer stable tag exists) is named for removal WITHOUT failing — it
// suppresses nothing, and a hard stale-fail would re-open the exact
// red-window this file closes.
let burned =
(File.read "ci/release-burned.yaml"
|> from yaml {| burned: seq<{| tag: string; reason: string |}> |})
.burned
|> Seq.freeze

let burnedTags = burned |> Seq.map (fun e -> e.tag) |> Seq.freeze

// tags come from the checkout (the release tag is pushed before the
// draft is published). Only STABLE tags count [D:tag-immutability]: a
// prerelease suffix (v0.0.0-rc1) is EXCLUDED from releases/latest by
Expand All @@ -21,10 +40,11 @@ let repo = "weir-shell/weir"
let tags =
git tag -l "v*"
|> Seq.where (fun t -> not (Str.contains "-" t))
|> Seq.where (fun t -> not (Seq.contains t burnedTags))
|> Seq.freeze

if Seq.isEmpty tags then
print "no stable v* tag yet — nothing to check (pre-first-release; prerelease tags are exempt)"
print "no stable unburned v* tag yet — nothing to check (pre-first-release; prerelease and burned tags are exempt)"
exit 0

// newest by VERSION, not by text — v0.0.9 sorts after v0.0.31 textually
Expand All @@ -39,13 +59,24 @@ let newest = tags |> Seq.maxBy verKey
// GitHub's API requires a User-Agent; the default one [D:http-ua] covers it.
// GITHUB_TOKEN when present: CI runners share egress IPs, so the
// unauthenticated rate limit 403s routinely — auth lifts it.
let req = Http.get $"https://api.github.com/repos/{repo}/releases/latest"

let rel =
let sendGh req =
match Env.get "GITHUB_TOKEN" with
| Some t -> Http.send { req with auth = Bearer (Secret.of t) }
| None -> Http.send req

// the burned sweep: a still-current entry must reference a tag that is
// genuinely unpublished — /releases/tags/{tag} answers 200 for
// PUBLISHED only (a draft has no tag association until the click)
for e in burned do
if verKey e.tag < verKey newest then
print $"note: burned entry {e.tag} ('{e.reason}') aged out — {newest} supersedes it; remove it at leisure"
else
let r = sendGh (Http.get $"https://api.github.com/repos/{repo}/releases/tags/{e.tag}")
if r.status == 200 then
fail $"ci/release-burned.yaml lists {e.tag} ('{e.reason}') as burned but that release IS published — stale entry, remove it"

let rel = sendGh (Http.get $"https://api.github.com/repos/{repo}/releases/latest")

if rel.status == 200 then
let latest = (rel.body |> from json {| tag_name: string |}).tag_name
if latest == newest then
Expand Down Expand Up @@ -81,7 +112,7 @@ if rel.status == 200 then
weir ci/release-assets.weir --tag $latest | orFail $"releases/latest {latest} is published but INCOMPLETE (verdicts above) — a published release is immutable, so nothing repairs {latest} in place: fix forward, cut and publish release vNEXT (the supersede path) and acknowledge {latest} in ci/release-known-incomplete.yaml"

exit 0
fail $"releases/latest is {latest} but the newest stable tag is {newest} — that tag was pushed and never PUBLISHED, and the installers silently keep serving {latest}. If its draft exists: gh release edit {newest} --draft=false (or the Releases page); if the release run never built (a red gate), fix and re-release — or burn the tag by cutting the next one"
fail $"releases/latest is {latest} but the newest stable tag is {newest} — that tag was pushed and never PUBLISHED, and the installers silently keep serving {latest}. If its draft exists: gh release edit {newest} --draft=false (or the Releases page); if the release run never built (a red gate), fix and re-release — or burn the tag: acknowledge it in ci/release-burned.yaml (tag + reason) and cut the next release; the gate then demands the newest unburned tag"

// a 404 is the missing-publish state with NO release published yet —
// /latest excluding drafts answers 404, never 403. Any other status is
Expand Down
Loading
Loading