Skip to content

Commit 2f88a09

Browse files
authored
Add run pr and download pr to toolkit (nushell#16770)
This PR adds the `toolkit run pr` and `toolkit download pr` commands to `toolkit`. This is a more fleshed out version of the snippet shared in nushell#16633, with robust error handling and cross-platform unzip support. When using `toolkit run pr`, the script will also check if the most recent binary for that PR has already been downloaded, and if so it will run that instead. I tried to make the error reporting as good as a built-in command to see how difficult that would be, and with use of the `--head: oneof<>` trick, it turned out pretty good. With access to the call span, the workflow is very similar to when writing a built-in command. I also used a `Spanned`-like record, which helped as well. ```nushell toolkit run pr 16740 # => Error: nu::shell::error # => # => × Command not found # => ╭─[entry nushell#4:1:26] # => 1 │ overlay use -pr toolkit; toolkit run pr 16740 # => · ───────┬────── # => · ╰── requires `gh` # => ╰──── # => help: Please install the `gh` commandline tool ``` <details> <summary>More error reporting notes</summary> In an earlier version of the script, `run pr` called `download pr` directly. I ended up changing the way this worked so that `run pr` could use the workflow_id. Here's a couple snippets I thought were neat from this older version. &nbsp; **Passing span via `--head`:** ```nushell def download [--head: oneof<>] { let span = $head | default (metadata $head).span error make {msg: "a", label: {text: here, span: $span}} } def run [--head: oneof<>] { let span = (metadata $head).span download --head=$span } download # => Error: nu:🐚:error # => # => × a # => ╭─[entry nushell#6:1:1] # => 1 │ download # => · ────┬─── # => · ╰── here # => ╰──── run # => Error: nu:🐚:error # => # => × a # => ╭─[entry nushell#7:1:1] # => 1 │ run # => · ─┬─ # => · ╰── here # => ╰──── ``` **Using "spanned" number as CLI parameter and as internal caller parameter** ```nushell def download [number: oneof<int, record<item: int, span: record>>] { let number = match $number { {item: $_, span: $_} => $number, $val => {item: $number, span: (metadata $number).span} } error make {msg: "a", label: {text: here, span: $number.span}} } def run [number: int] { let number = {item: $number, span: (metadata $number).span} download $number } download 123 # => Error: nu:🐚:error # => # => × a # => ╭─[entry nushell#9:1:10] # => 1 │ download 123 # => · ─┬─ # => · ╰── here # => ╰──── run 123 # => Error: nu:🐚:error # => # => × a # => ╭─[entry nushell#10:1:5] # => 1 │ run 123 # => · ─┬─ # => · ╰── here # => ╰──── ``` </details> ## Release notes summary - What our users need to know The toolkit in the Nushell repository can now download and run PRs by downloading artifacts from CI runs. It can be run like this: ```nushell use toolkit toolkit run pr <number> ```
1 parent 2bc71fb commit 2f88a09

File tree

4 files changed

+256
-0
lines changed

4 files changed

+256
-0
lines changed

toolkit/artifact/api.nu

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
use std-rfc/iter only
2+
use std/util null_device
3+
4+
# Get the workflow of the most recent commit with artifacts
5+
export def get-workflow-run [commits: list<string>, span: record]: nothing -> int {
6+
mut latest = true
7+
for commit in ($commits | reverse) {
8+
let checks = (
9+
^gh api $"/repos/nushell/nushell/commits/($commit)/check-runs"
10+
| from json
11+
| get check_runs
12+
| where name starts-with 'std-lib'
13+
)
14+
15+
if ($checks | is-empty) {
16+
$latest = false
17+
continue
18+
}
19+
20+
if not $latest {
21+
let short = $commit | str substring 0..6
22+
print $"(ansi yellow)Warning: using most recent commit with artifacts ($short), which is not the most recent on the PR(ansi reset)"
23+
}
24+
25+
return (
26+
$checks
27+
| first
28+
| get html_url
29+
# parse workflow id from url to avoid another request
30+
| parse "https://github.com/nushell/nushell/actions/runs/{workflow_id}/job/{job_id}"
31+
| only workflow_id
32+
)
33+
}
34+
35+
error make {
36+
msg: $"No artifacts"
37+
label: {
38+
text: $"no commits matching criteria have artifacts"
39+
span: $span
40+
}
41+
help: "Note that artifacts are deleted after 14 days"
42+
}
43+
44+
# BUG: Unreachable echo to appease parse-time type checking
45+
echo
46+
}
47+
48+
# Get artifacts associated with a PR
49+
#
50+
# Uses the latest commit if not specified
51+
export def get-artifacts [
52+
number: record<item: int, span: record>
53+
platform: string
54+
span: record<start: int, end: int>
55+
--commit: string
56+
] {
57+
# Make sure gh is available and has auth set up
58+
if (which ^gh | is-empty) {
59+
error make {
60+
msg: "Command not found"
61+
label: {
62+
text: "requires `gh`"
63+
span: $span
64+
}
65+
help: "Please install the `gh` commandline tool"
66+
}
67+
}
68+
69+
try {
70+
^gh auth status --hostname github.com o> $null_device
71+
} catch {
72+
error make {
73+
msg: "No authentication"
74+
label: {
75+
text: "requires GitHub authentication"
76+
span: $span
77+
}
78+
help: "Please run `gh auth login`"
79+
}
80+
}
81+
82+
# Listing all artifacts requires pagination, which results in 8+ requests
83+
# Instead, we can do PR -> commit -> check runs -> artifacts which always is 4 requests
84+
85+
# Get latest commit from PR (or use --commit)
86+
let commits = (
87+
^gh pr view $number.item -R nushell/nushell --json commits
88+
| from json
89+
| get commits.oid
90+
| if $commit != null { where $it == $commit } else {}
91+
)
92+
93+
let workflow_id = get-workflow-run $commits $number.span
94+
let artifacts = (
95+
^gh api $"/repos/nushell/nushell/actions/runs/($workflow_id)/artifacts"
96+
| from json
97+
| get artifacts
98+
| into datetime created_at
99+
| sort-by -r created_at
100+
| where name == $"nu-($number.item)-($platform)"
101+
)
102+
103+
if ($artifacts | is-empty) {
104+
error make {
105+
msg: $"No artifacts"
106+
label: {
107+
text: $"no artifacts for PR match criteria"
108+
span: $span
109+
}
110+
help: "Note that artifacts are deleted after 14 days"
111+
}
112+
}
113+
114+
$artifacts
115+
}

toolkit/artifact/mod.nu

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
use api.nu *
2+
use unzip.nu
3+
4+
# Download a Nushell binary from a pull request CI artifact
5+
export def "download pr" [
6+
# The PR number to download the Nushell binary from
7+
number: int
8+
# Use specific commit from branch
9+
--commit: string
10+
# Which platform to download for
11+
--platform: string
12+
# For internal use only
13+
--head: oneof<>
14+
]: nothing -> binary {
15+
let span = (metadata $head).span
16+
let number = { item: $number, span: (metadata $number).span }
17+
18+
let platform = get-platform $span $platform
19+
let artifacts = get-artifacts $number $platform $span --commit=$commit | first
20+
21+
^gh api $artifacts.archive_download_url | unzip "nu" $span
22+
}
23+
24+
# Run Nushell by downloading a CI artifact from a pull request
25+
export def --wrapped "run pr" [
26+
# The PR number to download the Nushell binary from
27+
number: int
28+
# Use specific commit from branch
29+
--commit: string
30+
# Arguments to pass to Nushell
31+
...$rest
32+
# For internal use only
33+
--head: oneof<>
34+
]: nothing -> nothing {
35+
let span = (metadata $head).span
36+
let number = { item: $number, span: (metadata $number).span }
37+
38+
let dir = $nu.temp-path | path join "nushell-run-pr"
39+
mkdir $dir
40+
41+
let platform = get-platform $span
42+
let artifact = get-artifacts $number $platform $span --commit=$commit | first
43+
44+
let workflow_id = $artifact.workflow_run.id
45+
let binfile = $dir | path join $"nu-($number.item)-($workflow_id)"
46+
47+
if ($binfile | path exists) {
48+
print $"Using previously downloaded binary from workflow run ($workflow_id)"
49+
} else {
50+
print $"Downloading binary from workflow run ($workflow_id)..."
51+
^gh api $artifact.archive_download_url
52+
| unzip "nu" $span
53+
| save -p $binfile
54+
}
55+
56+
if $nu.os-info.family == "unix" {
57+
chmod +x $binfile
58+
}
59+
60+
^$binfile ...$rest
61+
}
62+
63+
def get-platform [span: record, platform?: string] {
64+
match $nu.os-info.name {
65+
_ if $platform != null => $platform
66+
"linux" => "ubuntu-22.04"
67+
"macos" => "macos-latest"
68+
"windows" => "windows-latest"
69+
$platform => {
70+
error make {
71+
msg: "Unsupported platform",
72+
label: {
73+
text: $"($platform) not supported"
74+
span: $span
75+
}
76+
}
77+
}
78+
}
79+
}

toolkit/artifact/unzip.nu

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# Cross-platform unzipping for artifacts
2+
export def main [
3+
filename: string, # Name of file within zip to extract
4+
span: record # Span for error reporting
5+
]: binary -> binary {
6+
# Store zip file to temporary file
7+
let zipfile = do {|file| save -fp $file; $file } (mktemp -t)
8+
9+
let programs = [
10+
[preconditions, closure];
11+
[(which "gzip" | is-not-empty), { gzip $zipfile }]
12+
[((which "tar" | is-not-empty) and $nu.os-info.name == "windows"), { tar $zipfile $filename }]
13+
[(which "7z" | is-not-empty), { 7z $zipfile $filename }]
14+
[(which "unzip" | is-not-empty), { unzip $zipfile $filename }]
15+
]
16+
17+
# Attempt available programs
18+
for program in $programs {
19+
if not $program.preconditions {
20+
continue
21+
}
22+
23+
try {
24+
let out = do $program.closure
25+
rm $zipfile
26+
return $out
27+
}
28+
}
29+
30+
error make {
31+
msg: "Command not found"
32+
help: "Install one of the following programs: gzip, 7z, unzip, tar (Windows only)"
33+
label: {
34+
text: "failed to unzip artifact"
35+
span: $span
36+
}
37+
}
38+
39+
# BUG: Unreachable echo to appease parse-time type checking
40+
echo
41+
}
42+
43+
# tar can unzip files on Windows
44+
def tar [zipfile: string, filename: string] {
45+
^tar -Oxf $zipfile $filename
46+
}
47+
48+
# Some versions of gzip can extract single files from zip files
49+
def gzip [zipfile: string] {
50+
open -r $zipfile | ^gzip -d
51+
}
52+
53+
# Use 7zip
54+
def 7z [zipfile: string, filename: string] {
55+
^7z x $zipfile -so $filename
56+
}
57+
58+
# Use unzip tool (Info-ZIP, macOS, BSD)
59+
def unzip [zipfile: string, filename: string] {
60+
^unzip -p $zipfile $filename
61+
}

toolkit/mod.nu

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# (**2**) catch classical flaws in the new changes with *clippy* and (**3**)
77
# make sure all the tests pass.
88

9+
export use artifact *
910
export use benchmark.nu *
1011
export use checks.nu *
1112
export use coverage.nu *

0 commit comments

Comments
 (0)