Skip to content

Commit b59f606

Browse files
Merge pull request #496 from roc-lang/prepare-0.23.0-release
Prepare release notes for 0.23.0
2 parents 9668cf5 + 3cf85c1 commit b59f606

3 files changed

Lines changed: 189 additions & 1 deletion

File tree

‎.github/workflows/release.yml‎

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -365,7 +365,31 @@ jobs:
365365
with:
366366
name: platform-docs
367367

368-
- name: Make release notes
368+
- name: Select release notes
369+
id: release-notes
370+
env:
371+
RELEASE_VERSION: ${{ needs.build.outputs.release_version }}
372+
run: |
373+
if [[ -f "docs/releases/${RELEASE_VERSION}.md" ]]; then
374+
echo "custom=true" >> "$GITHUB_OUTPUT"
375+
else
376+
echo "custom=false" >> "$GITHUB_OUTPUT"
377+
fi
378+
379+
- name: Use repository release notes
380+
if: steps.release-notes.outputs.custom == 'true'
381+
uses: roc-lang/release-package/actions/make-release-notes@af2f0cd8662874b825ff3917f4286e793ba18c55
382+
with:
383+
release_version: ${{ needs.build.outputs.release_version }}
384+
release_notes_command: >-
385+
python3 scripts/render_release_notes.py
386+
"docs/releases/${RELEASE_VERSION}.md" "$RELEASE_NOTES_PATH"
387+
--repo "$GITHUB_REPOSITORY"
388+
--version "$RELEASE_VERSION"
389+
--bundles "$RELEASE_BUNDLES_PATH"
390+
391+
- name: Generate release notes
392+
if: steps.release-notes.outputs.custom != 'true'
369393
uses: roc-lang/release-package/actions/make-release-notes@af2f0cd8662874b825ff3917f4286e793ba18c55
370394
with:
371395
release_version: ${{ needs.build.outputs.release_version }}

‎docs/releases/0.23.0.md‎

Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
# basic-cli 0.23.0
2+
3+
```roc
4+
app [main!] { pf: platform "{{PLATFORM_URL}}" }
5+
```
6+
7+
Requires the Roc new-compiler nightly **`nightly-2026-09-22-e494788`** or later.
8+
9+
## Write the script in Roc, not bash
10+
11+
Most command-line programs eventually need a bash script alongside them. They call `mktemp -d` to get a scratch directory, `cp -r` to stage files, `timeout 300` to stop a stuck build, and a `trap` to clean up afterwards. Those scripts are hard to make portable, the errors come back as exit codes and text, and a child process can easily be left running.
12+
13+
0.23.0 gives basic-cli native APIs for this work, so the whole script can be a Roc program. Arguments are passed straight through without shell quoting. Exit codes, timeouts and output are returned as data you can `match` on. Processes, listeners and temporary directories are cleaned up when your program finishes with them.
14+
15+
Here is a release-staging script that would usually need `mktemp`, `cp -r` and a `trap`:
16+
17+
```roc
18+
Env.with_temp_dir!(|workspace| {
19+
staged = Path.join(workspace, "release")
20+
Path.copy_dir!(source, staged)?
21+
Path.write_utf8!(Path.join(staged, "RELEASE.txt"), "Prepared with basic-cli\n")?
22+
Path.copy_dir!(staged, destination)? # fails rather than overwrite an existing release
23+
Ok({})
24+
})?
25+
```
26+
27+
The temporary workspace is deleted whether the callback succeeds or fails. If both the callback and the cleanup fail, you get both errors, not just one. See the full [`filesystem-tools.roc`](https://github.com/roc-lang/basic-cli/blob/0.23.0/examples/filesystem-tools.roc) and [`process-control.roc`](https://github.com/roc-lang/basic-cli/blob/0.23.0/examples/process-control.roc) examples.
28+
29+
## Run and supervise processes
30+
31+
`Cmd` now covers what bash's `timeout`, `tee` and `&` are usually used for:
32+
33+
```roc
34+
output = Cmd.new("cargo")
35+
.args(["build", "--release"])
36+
.stdout(Tee) # stream to the terminal *and* capture
37+
.stderr(Tee)
38+
.timeout_ms(300_000) # hard deadline, including draining output
39+
.manage_tree(Bool.True) # on timeout, kill grandchildren too
40+
.run!()?
41+
42+
match output.status {
43+
Exited(0) => Stdout.line!("Build succeeded")
44+
Exited(code) => Err(BuildFailed(code))
45+
Signaled(signal) => Err(BuildKilled(signal))
46+
}
47+
```
48+
49+
- **`Cmd.run!`** runs a command to completion. A nonzero exit is returned as a status for you to handle, not as an error. Errors are reserved for failures of the command itself: `IO`, `Timeout(partial_output)` and `OutputLimit(partial_output)`. Both limit errors include the output collected before the command was stopped.
50+
- **`Cmd.spawn!`** starts a process in the background and returns a `Child` handle. Use `pid!`, `wait!`, `try_wait!`, `kill!` and `close!` to manage it. To interact with it, set `.stdin(Pipe)` and `.stdout(Pipe)`, then use `write!`, `read!` and `close_stdin!`.
51+
- **New builder options:** `cwd`, `stdin`, `stdout` and `stderr` (`Inherit`, `Null`, `Capture`, `Pipe`, `Tee`, and `Bytes` for stdin), `timeout_ms`, `output_limit`, `pending_limit`, `manage_tree` and `merge_stderr`.
52+
- **Cleanup is automatic.** When your program drops its last reference to a `Child`, the process is terminated and reaped. Call `close!` if you need this to happen at a specific point.
53+
54+
The existing `exec!`, `exec_cmd!`, `exec_output!` and `exec_output_bytes!` helpers now run on the same process engine.
55+
56+
## Files, paths and temporary workspaces
57+
58+
- `Path.copy!` copies a file, including its permissions.
59+
- `Path.copy_dir!` copies a directory tree to a destination that must not exist yet. `Path.copy_dir_with!` lets you choose whether symlinks are preserved or followed, and whether an existing destination is merged into. Copies that would loop, or where the source and destination overlap, are rejected.
60+
- `Path.absolute!` makes a path absolute without touching the filesystem. `Path.canonicalize!` resolves an existing path, including symlinks.
61+
- `Env.create_temp_dir!`, `Env.create_temp_dir_with_prefix!` and `Env.create_temp_dir_in!` create private temporary directories. `Env.with_temp_dir!` also deletes the directory when your callback finishes.
62+
- `OsStr.to_bytes` returns the raw native bytes of an OS string.
63+
64+
## Servers and timing
65+
66+
- **Accept TCP connections.** `Tcp.listen!(host, port, timeout_ms)` returns a `Tcp.Listener` with `accept!`, `local_port!` and `close!` methods. Listen on port `0` and read `local_port!` to have the OS pick a free port. This is useful for tests and local helper servers.
67+
- **Measure elapsed time.** `Monotonic.now!` returns nanoseconds (`U64`) from a clock that system clock changes don't affect. Subtract two readings to get a duration. The values are only meaningful within a single run of your program, so don't store them.
68+
69+
## Upgrading from 0.22
70+
71+
**`main!` no longer receives the program name.** `args` now contains only the arguments the user passed, which matches Roc's default platform ([#494](https://github.com/roc-lang/basic-cli/pull/494)). Code that skipped the first element will now **silently drop a real argument**, so check every place that handles `args`:
72+
73+
```roc
74+
# 0.22
75+
main! = |args| {
76+
user_args = args.drop_first(1)
77+
...
78+
}
79+
80+
# 0.23
81+
main! = |args| {
82+
# args are already the user's arguments.
83+
# Program name, if needed:
84+
name = Env.program_name!()? # Err(ProgramNameUnavailable) if the launcher didn't supply one
85+
...
86+
}
87+
```
88+
89+
**Remove `..` from error types in return position.** Roc now makes tag unions in return position open automatically, so basic-cli's signatures are now written without `..` (for example, `Path.read_bytes! : Path => Try(List(U8), [PathErr(IOErr, Path)])`), and yours can be too ([#495](https://github.com/roc-lang/basic-cli/pull/495)). This change is why 0.23.0 needs the nightly listed at the top.
90+
91+
**Captured output is capped at 16 MiB by default.** When stdout and stderr together exceed the limit, the command is cancelled. `run!` returns `OutputLimit(partial_output)`, and the `exec_output*` helpers return `FailedToGetExitCode` with the message "Process output limit exceeded". If a command legitimately produces more output, raise the limit with `.output_limit(bytes)`, or use `.stdout(Inherit)` to send the output straight to the terminal.
92+
93+
**Update the platform URL.** The native host interface changed to support managed resources, so programs built against 0.22 need to be rebuilt against 0.23.0. Mixing hosts and platform sources from different versions is not supported.
94+
95+
## Fixes
96+
97+
- **Windows:** programs no longer hang on exit if they made an HTTP request in the previous ten seconds ([#493](https://github.com/roc-lang/basic-cli/pull/493)).
98+
- File readers, TCP streams and SQLite statements now release their OS resources as soon as the last reference to them is dropped, instead of at program exit.
99+
- **SQLite:** fixed a type-inference problem in the row decoders that could cause errors in application code using them. Columns are now looked up by name ([#475](https://github.com/roc-lang/basic-cli/pull/475), 0.22.1).
100+
- `Path.to_str` errors now combine with other errors in the same function when you use `?` ([#479](https://github.com/roc-lang/basic-cli/pull/479), 0.22.2). With the implicit-open change above, every basic-cli function now behaves this way.
101+
102+
## Documentation and tooling
103+
104+
- The API docs have been revised, with runnable examples for the new process, filesystem and TCP APIs.
105+
- Contributors can use the new Nix flake devshell with direnv ([#432](https://github.com/roc-lang/basic-cli/pull/432)), and point the test runner at a specific compiler with `ROC=...` ([#477](https://github.com/roc-lang/basic-cli/pull/477)).
106+
107+
## Thanks
108+
109+
Thanks to everyone who contributed to 0.22.1 through 0.23.0: @ageron, @Anton-4, @Karakatiza666, @lukewilliamboswell, @niclas-ahden, @rtfeldman and @thebrandonlucas.
110+
111+
---
112+
113+
- [API documentation](https://roc-lang.github.io/basic-cli/0.23.0/)
114+
- [Full changelog since 0.22.0](https://github.com/roc-lang/basic-cli/compare/0.22.0...0.23.0)

‎scripts/render_release_notes.py‎

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
#!/usr/bin/env python3
2+
"""Render docs/releases/<version>.md, filling in values only known at release time.
3+
4+
Placeholders:
5+
{{PLATFORM_URL}} URL of the published platform bundle
6+
"""
7+
from __future__ import annotations
8+
9+
import argparse
10+
import json
11+
import re
12+
from pathlib import Path
13+
14+
15+
PLACEHOLDER_RE = re.compile(r"\{\{[A-Z_]+\}\}")
16+
17+
18+
def platform_url(repo: str, version: str, bundles_path: Path) -> str:
19+
bundles = json.loads(bundles_path.read_text(encoding="utf-8"))
20+
if len(bundles) != 1:
21+
raise SystemExit(f"expected exactly one release bundle, found {len(bundles)}")
22+
return f"https://github.com/{repo}/releases/download/{version}/{bundles[0]['artifact_file']}"
23+
24+
25+
def render(template: str, values: dict[str, str]) -> str:
26+
rendered = template
27+
for name, value in values.items():
28+
rendered = rendered.replace("{{" + name + "}}", value)
29+
30+
unknown = sorted(set(PLACEHOLDER_RE.findall(rendered)))
31+
if unknown:
32+
raise SystemExit(f"unknown release notes placeholders: {', '.join(unknown)}")
33+
return rendered
34+
35+
36+
def main() -> None:
37+
parser = argparse.ArgumentParser(description=__doc__)
38+
parser.add_argument("template", type=Path)
39+
parser.add_argument("output", type=Path)
40+
parser.add_argument("--repo", required=True)
41+
parser.add_argument("--version", required=True)
42+
parser.add_argument("--bundles", type=Path, required=True)
43+
args = parser.parse_args()
44+
45+
values = {"PLATFORM_URL": platform_url(args.repo, args.version, args.bundles)}
46+
args.output.write_text(render(args.template.read_text(encoding="utf-8"), values), encoding="utf-8")
47+
48+
49+
if __name__ == "__main__":
50+
main()

0 commit comments

Comments
 (0)