Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
2 changes: 1 addition & 1 deletion .github/workflows/homebrew.yml
Original file line number Diff line number Diff line change
Expand Up @@ -118,7 +118,7 @@ jobs:
VERSION="${RELEASE_TAG#v}"
gh release download "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --pattern SHA256SUMS
test -f SHA256SUMS
for triple in aarch64-apple-darwin x86_64-unknown-linux-gnu; do
for triple in aarch64-apple-darwin x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
asset="numan-${VERSION}-${triple}.tar.gz"
# Use gh --jq (built into gh) so we do not depend on a separate jq binary.
ok="$(
Expand Down
6 changes: 6 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -100,9 +100,15 @@ jobs:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
bin_name: numan
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
bin_name: numan
- os: windows-latest
target: x86_64-pc-windows-msvc
bin_name: numan.exe
- os: windows-11-arm
target: aarch64-pc-windows-msvc
bin_name: numan.exe
- os: macos-latest
target: aarch64-apple-darwin
bin_name: numan
Expand Down
16 changes: 9 additions & 7 deletions .github/workflows/winget.yml
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,25 @@ jobs:
--jq 'any(.artifacts[]; .name == "winget-release-ready" and .expired == false)' \
| grep -Fx true

- name: Verify published Windows release asset
- name: Verify published Windows release assets
env:
GH_TOKEN: ${{ github.token }}
shell: bash
run: |
set -euo pipefail
expected_asset="numan-${RELEASE_TAG#v}-x86_64-pc-windows-msvc.zip"
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \
| jq -e --arg expected_asset "$expected_asset" \
'.isDraft == false and any(.assets[]; .name == $expected_asset and .state == "uploaded")' \
>/dev/null
for triple in x86_64-pc-windows-msvc aarch64-pc-windows-msvc; do
expected_asset="numan-${RELEASE_TAG#v}-${triple}.zip"
gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --json isDraft,assets \
| jq -e --arg expected_asset "$expected_asset" \
'.isDraft == false and any(.assets[]; .name == $expected_asset and .state == "uploaded")' \
>/dev/null
done

- name: Publish manifest to WinGet
uses: vedantmgoyal9/winget-releaser@b3a5dae0047c6180023acba3f548c55fdf6b7193
with:
identifier: tonythethompson.numan
fork-user: tonythethompson
installers-regex: '\.zip$'
installers-regex: '-(x86_64|aarch64)-pc-windows-msvc\.zip$'
release-tag: ${{ env.RELEASE_TAG }}
token: ${{ secrets.WINGET_TOKEN }}
5 changes: 5 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

## [Unreleased]

### Added

- **Release assets** for `aarch64-unknown-linux-gnu` and `aarch64-pc-windows-msvc` (native ARM runners); `numan update --self` maps those triples
- **Homebrew** Linux ARM bottle URLs; **winget** verifies and submits both Windows x64 and ARM64 zips
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
Comment thread
tonythethompson marked this conversation as resolved.
Outdated

## [0.2.1] - 2026-08-07

### Added
Expand Down
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,9 @@ Download the latest archive for your platform from [GitHub Releases](https://git
| Platform | Archive | Binary |
|----------|---------|--------|
| Linux (x86_64) | `numan-<version>-x86_64-unknown-linux-gnu.tar.gz` | `numan` |
| Linux (aarch64) | `numan-<version>-aarch64-unknown-linux-gnu.tar.gz` | `numan` |
| Windows (x86_64) | `numan-<version>-x86_64-pc-windows-msvc.zip` | `numan.exe` |
| Windows (ARM64) | `numan-<version>-aarch64-pc-windows-msvc.zip` | `numan.exe` |
Comment thread
coderabbitai[bot] marked this conversation as resolved.
| macOS (Apple Silicon) | `numan-<version>-aarch64-apple-darwin.tar.gz` | `numan` |

**Linux / macOS**
Expand Down
12 changes: 6 additions & 6 deletions docs/PACKAGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,12 +47,12 @@ Release archives extract to `numan-<version>-<target>/` containing the `numan`

## Installation coverage

| Installation channel | Linux x86_64 | macOS Apple Silicon | Windows x86_64 | Windows ARM64 |
|-----------------------|---------------|---------------------|----------------|---------------|
| GitHub release archive | yes | yes | yes | no |
| `cargo install numan-cli` | source build | source build | source build | not validated |
| Homebrew tap | yes | yes | — | — |
| winget | — | — | yes | |
| Installation channel | Linux x86_64 | Linux aarch64 | macOS Apple Silicon | Windows x86_64 | Windows ARM64 |
|-----------------------|---------------|---------------|---------------------|----------------|---------------|
| GitHub release archive | yes | yes | yes | yes | yes |
| `cargo install numan-cli` | source build | source build | source build | source build | not validated |
| Homebrew tap | yes | yes | yes | — | — |
| winget | — | — | — | yes | yes |

## Secrets

Expand Down
2 changes: 1 addition & 1 deletion docs/RELEASING.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ Then:
```

6. The [Release workflow](https://github.com/tonythethompson/numan/actions/workflows/release.yml) waits for green CI on the tagged commit, runs preflight checks, then builds archives and publishes.
7. Confirm platform archives, `SHA256SUMS`, and `SHA256SUMS.sig` on GitHub Releases.
7. Confirm platform archives (`x86_64-unknown-linux-gnu`, `aarch64-unknown-linux-gnu`, `x86_64-pc-windows-msvc`, `aarch64-pc-windows-msvc`, `aarch64-apple-darwin`), `SHA256SUMS`, and `SHA256SUMS.sig` on GitHub Releases.
8. Confirm the **Publish to crates.io** job succeeds (requires Trusted Publishing / OIDC on crates.io).
9. Confirm the [`Publish to WinGet`](../.github/workflows/winget.yml) workflow verifies the `winget-release-ready` artifact and published Windows release asset, then opens the update PR after the `v*.*.*` tag-triggered Release workflow completes (manual recovery: dispatch with required `release_tag`).
10. Confirm the [`Publish to Homebrew tap`](../.github/workflows/homebrew.yml) workflow verifies the `homebrew-release-ready` artifact and pushes `Formula/numan.rb` to [`tonythethompson/homebrew-numan`](https://github.com/tonythethompson/homebrew-numan) (requires `HOMEBREW_TAP_TOKEN`; manual recovery: dispatch with required `release_tag`).
Expand Down
1 change: 1 addition & 0 deletions packaging/homebrew/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ Required release assets:

- `numan-<version>-aarch64-apple-darwin.tar.gz`
- `numan-<version>-x86_64-unknown-linux-gnu.tar.gz`
- `numan-<version>-aarch64-unknown-linux-gnu.tar.gz`
Comment thread
coderabbitai[bot] marked this conversation as resolved.

Intel Mac (`x86_64-apple-darwin`) is not shipped; the formula fails with a clear
`odie` on Intel Hardware.
Expand Down
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json

# Sample dual-arch installer shape. Live publishes are driven by winget-releaser.
# NestedInstallerFiles differ per Architecture (nested path includes the triple).
PackageIdentifier: tonythethompson.numan
PackageVersion: 0.1.3
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
Commands:
- numan
Installers:
- Architecture: x64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.3/numan-0.1.3-x86_64-pc-windows-msvc.zip
InstallerSha256: 90F659E665836F26E2FD45AD86ED8F73C1B6BCB24BE87CF3760EC000E094ABA1
- Architecture: arm64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.3-aarch64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.3/numan-0.1.3-aarch64-pc-windows-msvc.zip
# Placeholder: real digests are taken from SHA256SUMS on each tagged release.
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
Comment thread
tonythethompson marked this conversation as resolved.
Outdated
ManifestType: installer
ManifestVersion: 1.12.0
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json

# Sample dual-arch installer shape. Live publishes are driven by winget-releaser.
# NestedInstallerFiles differ per Architecture (nested path includes the triple).
PackageIdentifier: tonythethompson.numan
PackageVersion: 0.1.4
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
Commands:
- numan
Installers:
- Architecture: x64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.4/numan-0.1.4-x86_64-pc-windows-msvc.zip
InstallerSha256: F843A717BD7FD13F2D86BF727D2292DCF9831F2544A3DF88987104299229EF9D
- Architecture: arm64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.4-aarch64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.4/numan-0.1.4-aarch64-pc-windows-msvc.zip
# Placeholder: real digests are taken from SHA256SUMS on each tagged release.
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
ManifestType: installer
ManifestVersion: 1.12.0
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json

# Sample dual-arch installer shape. Live publishes are driven by winget-releaser.
# NestedInstallerFiles differ per Architecture (nested path includes the triple).
PackageIdentifier: tonythethompson.numan
PackageVersion: 0.1.3
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
Commands:
- numan
Installers:
- Architecture: x64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.3-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.3/numan-0.1.3-x86_64-pc-windows-msvc.zip
InstallerSha256: 90F659E665836F26E2FD45AD86ED8F73C1B6BCB24BE87CF3760EC000E094ABA1
- Architecture: arm64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.3-aarch64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.3/numan-0.1.3-aarch64-pc-windows-msvc.zip
# Placeholder: real digests are taken from SHA256SUMS on each tagged release.
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
ManifestType: installer
ManifestVersion: 1.12.0
Original file line number Diff line number Diff line change
@@ -1,17 +1,26 @@
# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.12.0.schema.json

# Sample dual-arch installer shape. Live publishes are driven by winget-releaser.
# NestedInstallerFiles differ per Architecture (nested path includes the triple).
PackageIdentifier: tonythethompson.numan
PackageVersion: 0.1.4
InstallerType: zip
NestedInstallerType: portable
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
Commands:
- numan
Installers:
- Architecture: x64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.4-x86_64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.4/numan-0.1.4-x86_64-pc-windows-msvc.zip
InstallerSha256: F843A717BD7FD13F2D86BF727D2292DCF9831F2544A3DF88987104299229EF9D
- Architecture: arm64
NestedInstallerFiles:
- RelativeFilePath: numan-0.1.4-aarch64-pc-windows-msvc\numan.exe
PortableCommandAlias: numan
InstallerUrl: https://github.com/tonythethompson/numan/releases/download/v0.1.4/numan-0.1.4-aarch64-pc-windows-msvc.zip
# Placeholder: real digests are taken from SHA256SUMS on each tagged release.
InstallerSha256: 0000000000000000000000000000000000000000000000000000000000000000
ManifestType: installer
ManifestVersion: 1.12.0
8 changes: 7 additions & 1 deletion scripts/render_homebrew_formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,12 @@
REQUIRED_ASSETS = {
"aarch64-apple-darwin": "macos_arm",
"x86_64-unknown-linux-gnu": "linux_intel",
"aarch64-unknown-linux-gnu": "linux_arm",
}

ASSET_RE = re.compile(
r"^([0-9a-fA-F]{64})\s+numan-(?P<ver>.+)-(?P<triple>"
r"aarch64-apple-darwin|x86_64-unknown-linux-gnu"
r"aarch64-apple-darwin|x86_64-unknown-linux-gnu|aarch64-unknown-linux-gnu"
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated
r")\.(?P<ext>tar\.gz|zip)$"
)

Expand Down Expand Up @@ -57,6 +58,7 @@ def parse_sha256sums(text: str, version: str) -> dict[str, str]:
def render_formula(version: str, digests: dict[str, str]) -> str:
mac_arm = digests["aarch64-apple-darwin"]
linux_intel = digests["x86_64-unknown-linux-gnu"]
linux_arm = digests["aarch64-unknown-linux-gnu"]
return f"""# typed: false
# frozen_string_literal: true

Expand Down Expand Up @@ -91,6 +93,10 @@ class Numan < Formula
url "https://github.com/tonythethompson/numan/releases/download/v#{{version}}/numan-#{{version}}-x86_64-unknown-linux-gnu.tar.gz"
sha256 "{linux_intel}"
end
on_arm do
url "https://github.com/tonythethompson/numan/releases/download/v#{{version}}/numan-#{{version}}-aarch64-unknown-linux-gnu.tar.gz"
sha256 "{linux_arm}"
end
Comment thread
qodo-code-review[bot] marked this conversation as resolved.
Outdated
end

def install
Expand Down
19 changes: 19 additions & 0 deletions scripts/test_render_homebrew_formula.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,27 @@ def test_parse_and_render(self):
4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8 numan-0.1.5-aarch64-apple-darwin.tar.gz
2d855b3b8a9bb3c568051024b8aae9771a63c427cb3edfd3ac3aa3aa6d78468d numan-0.1.5-x86_64-pc-windows-msvc.zip
0b113361c189a2062ef6e1fca36795d2347b925c1862b42c4ddeb54773e00ae3 numan-0.1.5-x86_64-unknown-linux-gnu.tar.gz
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.1.5-aarch64-unknown-linux-gnu.tar.gz
""".strip()
digests = self.mod.parse_sha256sums(sums, "0.1.5")
self.assertEqual(
digests["aarch64-apple-darwin"],
"4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8",
)
self.assertEqual(
digests["aarch64-unknown-linux-gnu"],
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
)
text = self.mod.render_formula("0.1.5", digests)
self.assertIn('version "0.1.5"', text)
self.assertIn("numan-#{version}-aarch64-apple-darwin.tar.gz", text)
self.assertIn("numan-#{version}-aarch64-unknown-linux-gnu.tar.gz", text)
self.assertIn(
"4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8", text
)
self.assertIn(
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", text
)
self.assertIn('bin.install "#{arch_dir}/numan"', text)
self.assertIn("on_intel do", text)
self.assertIn("no longer ships Intel Mac", text)
Expand All @@ -53,12 +62,17 @@ def test_parse_prerelease_version(self):
sums = """
4D8FA065B5BC7FCCE30AF3CA7D5C3CD943701BEE168D78FAE6120A12689738B8 numan-0.2.0-beta.1-aarch64-apple-darwin.tar.gz
2d855b3b8a9bb3c568051024b8aae9771a63c427cb3edfd3ac3aa3aa6d78468d numan-0.2.0-beta.1-x86_64-unknown-linux-gnu.tar.gz
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.2.0-beta.1-aarch64-unknown-linux-gnu.tar.gz
""".strip()
digests = self.mod.parse_sha256sums(sums, "0.2.0-beta.1")
self.assertEqual(
digests["aarch64-apple-darwin"],
"4d8fa065b5bc7fcce30af3ca7d5c3cd943701bee168d78fae6120a12689738b8",
)
self.assertEqual(
digests["aarch64-unknown-linux-gnu"],
"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb",
)

def test_missing_asset_fails(self):
sums = "abcd numan-0.1.5-aarch64-apple-darwin.tar.gz\n"
Expand All @@ -69,6 +83,7 @@ def test_write_roundtrip(self):
sums = """
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa numan-0.2.0-aarch64-apple-darwin.tar.gz
cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc numan-0.2.0-x86_64-unknown-linux-gnu.tar.gz
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.2.0-aarch64-unknown-linux-gnu.tar.gz
""".strip()
with tempfile.TemporaryDirectory() as tmp:
sums_path = Path(tmp) / "SHA256SUMS"
Expand All @@ -81,6 +96,7 @@ def test_write_roundtrip(self):
body = out.read_text(encoding="utf-8")
self.assertEqual(body, text)
self.assertIn('version "0.2.0"', body)
self.assertIn("aarch64-unknown-linux-gnu.tar.gz", body)

def test_cli_check_url_layout(self):
buf = io.StringIO()
Expand All @@ -90,12 +106,14 @@ def test_cli_check_url_layout(self):
stdout = buf.getvalue()
self.assertIn("numan-<version>-aarch64-apple-darwin.tar.gz", stdout)
self.assertIn("numan-<version>-x86_64-unknown-linux-gnu.tar.gz", stdout)
self.assertIn("numan-<version>-aarch64-unknown-linux-gnu.tar.gz", stdout)
self.assertNotIn("x86_64-apple-darwin", stdout)

def test_cli_full_write(self):
sums = """
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa numan-0.2.0-aarch64-apple-darwin.tar.gz
cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc numan-0.2.0-x86_64-unknown-linux-gnu.tar.gz
bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb numan-0.2.0-aarch64-unknown-linux-gnu.tar.gz
""".strip()
with tempfile.TemporaryDirectory() as tmp:
tmp_path = Path(tmp)
Expand All @@ -119,6 +137,7 @@ def test_cli_full_write(self):
self.assertTrue(out_path.is_file())
text = out_path.read_text(encoding="utf-8")
self.assertIn('version "0.2.0"', text)
self.assertIn("aarch64-unknown-linux-gnu.tar.gz", text)
self.assertIn("Wrote ", buf.getvalue())

def test_cli_missing_required_args_fails(self):
Expand Down
Loading
Loading