From 647f808cc93da9b718cbdc49e2e6a8d044e5fb24 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 15:52:56 +0700 Subject: [PATCH 1/4] feat(distribution): latest release onboarding, Linux AppImage CI, Windows verified installer and WinGet manifest tooling --- .../arstack-studio-linux-appimage.yml | 157 ++++++++++++++++++ README.md | 48 ++++-- apps/arstack_studio/README.md | 18 +- docs/INSTALL.md | 49 ++++++ ...OCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md | 2 +- scripts/check_product_naming.py | 38 +++-- scripts/generate_winget_manifest.py | 85 ++++++++++ scripts/install-linux.sh | 43 +++++ scripts/install-windows.ps1 | 46 +++++ 9 files changed, 448 insertions(+), 38 deletions(-) create mode 100644 .github/workflows/arstack-studio-linux-appimage.yml create mode 100644 docs/INSTALL.md create mode 100644 scripts/generate_winget_manifest.py create mode 100644 scripts/install-linux.sh create mode 100644 scripts/install-windows.ps1 diff --git a/.github/workflows/arstack-studio-linux-appimage.yml b/.github/workflows/arstack-studio-linux-appimage.yml new file mode 100644 index 000000000..1c7c094a8 --- /dev/null +++ b/.github/workflows/arstack-studio-linux-appimage.yml @@ -0,0 +1,157 @@ +name: ARStack Studio Linux AppImage + +on: + pull_request: + paths: + - 'apps/arstack_studio/**' + - '.github/workflows/arstack-studio-linux-appimage.yml' + - 'scripts/install-linux.sh' + workflow_dispatch: + inputs: + release_tag: + description: 'Existing GitHub release tag to build, test and attach Linux AppImage; blank = build artifact only' + required: false + type: string + +permissions: + contents: read + +jobs: + appimage: + name: Build and test Linux x86_64 package + runs-on: ubuntu-22.04 + timeout-minutes: 55 + permissions: + contents: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && 'write' || 'read' }} + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && inputs.release_tag || github.sha }} + fetch-depth: 0 + + - name: Confirm release identity before packaging + if: ${{ inputs.release_tag != '' }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.release_tag }} + run: | + set -euo pipefail + test "$(git rev-parse HEAD)" = "$(git rev-parse "$TAG^{commit}")" + gh release view "$TAG" --json isDraft,isPrerelease -q 'select(.isDraft==false and .isPrerelease==false)' > release-check.json + test -s release-check.json + + - name: Install Linux native dependencies + run: | + sudo apt-get update + sudo apt-get install -y --no-install-recommends cmake ninja-build build-essential \ + libgl1-mesa-dev libxkbcommon-x11-0 libxcb-cursor0 libxcb-xinerama0 \ + libxcb-xkb1 libxcb-randr0 libxcb-image0 libxcb-keysyms1 libxcb-render-util0 \ + libfuse2 unzip xvfb libdbus-1-3 + + - name: Install Qt 6 + uses: jurplel/install-qt-action@v4 + with: + version: '6.10.3' + arch: gcc_64 + modules: 'qtserialport' + cache: true + + - name: Compile native Linux Studio and smoke test + run: | + set -euo pipefail + cmake -S apps/arstack_studio -B build-studio-linux -G Ninja \ + -DCMAKE_BUILD_TYPE=Release -DARIEC61850_WARNINGS_AS_ERRORS=ON + cmake --build build-studio-linux --target arstack_studio --parallel 2 + test -x build-studio-linux/arstack_studio + QT_QPA_PLATFORM=offscreen xvfb-run -a build-studio-linux/arstack_studio --check-reference-template + QT_QPA_PLATFORM=offscreen xvfb-run -a build-studio-linux/arstack_studio --check-p0-policy + + - name: Stage Linux app and matched optional release firmware + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.release_tag }} + run: | + set -euo pipefail + mkdir -p AppDir/usr/bin AppDir/usr/share/icons/hicolor/512x512/apps + cp build-studio-linux/arstack_studio AppDir/usr/bin/ARStackStudio + cp assets/studio_icon/android-chrome-512x512.png AppDir/usr/share/icons/hicolor/512x512/apps/arstack-studio.png + cat > arstack-studio.desktop <<'DESKTOP' + [Desktop Entry] + Type=Application + Name=ARStack Studio + Exec=ARStackStudio + Icon=arstack-studio + Categories=Education;Science;Electronics; + Terminal=false + DESKTOP + cp LICENSE NOTICE COPYRIGHT.md THIRD_PARTY_NOTICES.md AppDir/usr/bin/ + if [ -n "$TAG" ]; then + mkdir -p release-firmware AppDir/usr/bin/firmware AppDir/usr/bin/tools + gh release download "$TAG" --dir release-firmware --pattern 'firmware-manifest.json' --pattern '*.bin' + python3 - <<'PY' + import hashlib,json,pathlib,shutil + root=pathlib.Path('release-firmware') + manifest=json.loads((root/'firmware-manifest.json').read_text()) + binfiles=list(root.glob('*.bin')) + assert len(binfiles)==1 and manifest['chip']=='esp32p4', 'Release firmware ambiguity' + src=binfiles[0] + assert hashlib.sha256(src.read_bytes()).hexdigest()==manifest['sha256'], 'Firmware hash mismatch' + output=pathlib.Path('AppDir/usr/bin/firmware') + shutil.copy2(src,output/manifest['image']) + shutil.copy2(root/'firmware-manifest.json',output/'firmware-manifest.json') + (output/'SHA256SUMS.txt').write_text(f"{manifest['sha256']} {manifest['image']}\n") + PY + curl -fLsS https://github.com/esp-rs/espflash/releases/download/v4.5.0/espflash-x86_64-unknown-linux-gnu.zip -o espflash.zip + echo 'dcd7fd822f247df18966935bbb10c7cb4b700c87172349633e9e323ee2542717 espflash.zip' | sha256sum -c - + unzip -j espflash.zip 'espflash' -d AppDir/usr/bin/tools + chmod +x AppDir/usr/bin/tools/espflash + fi + + - name: Bundle Qt and create single-file AppImage + uses: miurahr/install-linuxdeploy-action@v1.8.0 + id: linuxdeploy + with: + plugins: qt appimage + + - name: Generate AppImage and release checksum + env: + QML_SOURCES_PATHS: ${{ github.workspace }}/apps/arstack_studio/qml + VERSION: ${{ inputs.release_tag || 'preview' }} + run: | + set -euo pipefail + export QMAKE="$(command -v qmake6 || command -v qmake)" + ${{ steps.linuxdeploy.outputs.linuxdeploy }} \ + --appdir AppDir \ + --executable AppDir/usr/bin/ARStackStudio \ + --desktop-file arstack-studio.desktop \ + --icon-file assets/studio_icon/android-chrome-512x512.png \ + --plugin qt --output appimage + shopt -s nullglob + images=( ./*.AppImage ) + test "${#images[@]}" -eq 1 + mv "${images[0]}" ARStack-Studio-linux-x86_64.AppImage + chmod +x ARStack-Studio-linux-x86_64.AppImage + APPIMAGE_EXTRACT_AND_RUN=1 QT_QPA_PLATFORM=offscreen \ + xvfb-run -a ./ARStack-Studio-linux-x86_64.AppImage --check-reference-template + sha256sum ARStack-Studio-linux-x86_64.AppImage > ARStack-Studio-linux-x86_64.sha256 + + - name: Upload reviewed Linux artifacts + uses: actions/upload-artifact@v4 + with: + name: arstack-studio-linux-x86_64 + path: | + ARStack-Studio-linux-x86_64.AppImage + ARStack-Studio-linux-x86_64.sha256 + if-no-files-found: error + retention-days: 14 + + - name: Add Linux artifacts to matching existing stable release + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' }} + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ inputs.release_tag }} + run: | + set -euo pipefail + gh release upload "$TAG" \ + ARStack-Studio-linux-x86_64.AppImage \ + ARStack-Studio-linux-x86_64.sha256 diff --git a/README.md b/README.md index d1610a3fa..ca5c853b1 100644 --- a/README.md +++ b/README.md @@ -1,30 +1,46 @@ -# ARStack61850 — Real-Time IEC 61850 Instrumentation Stack +# ARStack61850 — Open-source IEC 61850 Engineering Lab -ARStack61850 is the real-time, hardware-aware evolution of ARIEC61850. +Explore, simulate and test IEC 61850 in your own engineering lab. ARStack61850 combines a portable C++ protocol engine, desktop tools and real-time ESP32-P4 Sampled Values (SV) generation with PTP clock simulation. -> **ARIEC61850 understands IEC 61850. ARStack61850 measures and executes IEC 61850 in real time.** +**[Download the latest release](https://github.com/masarray/arstack61850/releases/latest)** · **[Open the product website](https://masarray.github.io/arstack61850/)** · **[Installation guide](docs/INSTALL.md)** · **[Source code](https://github.com/masarray/arstack61850)** -The portable C++20 stack remains rooted in the ARIEC61850 behavioral model, but the project north star is now broader: build an open, deterministic IEC 61850 instrumentation platform for hardware-timestamped Sampled Values/GOOSE/PTP injection, analysis, commissioning and real-time closed-loop testing. +### Get started -## Public release — Process Bus Simulator M3.H (2026-10-24) +1. Open **Latest release** and download the **Windows Setup** file; the portable package is also available. +2. Install **ARStack Studio** and follow the built-in first-run flow. +3. For real Ethernet SV/PTP experiments, connect a compatible ESP32-P4 Ethernet board and use the **firmware supplied with that same release**. You can explore the desktop interface before connecting hardware. +4. Use a dedicated lab Ethernet connection for SV and PTP experiments. -**Current public laboratory edition: [M3.H Public Stable](https://github.com/masarray/arstack61850/releases/tag/processbus-sim-m3h-20261024).** This is an open-source engineering simulator, **not** a certified merging unit or UTC time reference. No GPS, Omicron, or protection relay is required for an isolated-lab experiment. +**Windows command-line install** (downloads the current stable installer and verifies its SHA-256): -**[Download Setup EXE](https://github.com/masarray/arstack61850/releases/download/processbus-sim-m3h-20261024/ARStack-ProcessBus-M3H-win-x64-setup.exe)** · **[Portable ZIP](https://github.com/masarray/arstack61850/releases/download/processbus-sim-m3h-20261024/ARStack-ProcessBus-M3H-win-x64-portable.zip)** · **[ESP32-P4 firmware](https://github.com/masarray/arstack61850/releases/download/processbus-sim-m3h-20261024/ARStack-ESP32P4-ProcessBus-M3H-firmware.bin)** · [Release notes and engineer quick start](docs/releases/PROCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md) +```powershell +$installer = Join-Path $env:TEMP 'arstack-install.ps1' +Invoke-WebRequest 'https://raw.githubusercontent.com/masarray/arstack61850/main/scripts/install-windows.ps1' -OutFile $installer +& $installer +``` + +**Linux:** An AppImage packaging workflow is available; the [latest release](https://github.com/masarray/arstack61850/releases/latest) lists a Linux download once its build and checks pass. See [Linux install instructions](docs/INSTALL.md). Windows remains the verified desktop installer for the M3.H release. + +### What you can explore + +- **Sampled Values:** 4 currents + 4 voltages, simulated waveforms and real-time Ethernet output from supported ESP32-P4 hardware. +- **PTP timing:** an internal simulated grandmaster, time-sync controls and on-wire timing evidence. +- **Engineering tools:** SCL, COMTRADE, MMS, GOOSE and other IEC 61850 foundations in the source tree. Features depend on the chosen application and release. +- **Reproducible experimentation:** open-source code, tests, example configurations and release notes. + +No external GPS receiver or protection relay is needed to begin exploring the simulator. Network traffic generation is intended for a separate test bench, not an energized operational substation network. + +### Verified M3.H milestone -* **ESP32-P4 Ethernet:** 4I+4V IEC 61850 SV publisher, one ASDU, 64-byte samples, 4,000 frames/s, continuous `smpCnt 0–3999` with explicit START/STOP. -* **IEEE 1588 / IEC/IEEE 61850-9-3-style PTP emulation:** isolated-bench **internal simulated Global GM**, Layer-2 Announce/Sync/Follow_Up at approximately 1 Hz, ESP32-P4 EMAC hardware transmit timestamp, phase servo and MAC egress evidence. `smpSynch AUTO` supports `smpSynch=2` after local measured evidence; not an assertion of GPS/UTC traceability. -* **ARStack Studio on Windows:** 4I+4V waveform/phasor controls, capture/reporting, SCL input within supported profile, supervised firmware flashing, verified COM identity and USB hotplug recovery. -* **Actual independent M3.H capture:** **163,048 SV in 40.763 seconds**, 100% `smpSynch=2`, **zero missing, duplicate, counter-reset or malformed SV**, measured **3999.884 fps**, plus 44 PTP Announce and 43 each Sync/Follow_Up. A STOP truncated the requested 120-second capture; 120-second endurance and physical relay locking were **not** tested. -* **Source pinned:** `a922e9bdba4e57a4e88406eb83344455dd8ff142`; **10/10 CI workflows PASS** for the shipped Studio/firmware pairing. The public tag includes only documentation/packaging deltas from that tested build. -* **Release = main:** [PR #299](https://github.com/masarray/arstack61850/pull/299) merged the exact tested source and the public release-tag ancestry into `main` with **14/14 integration CI PASS**; no SV/PTP source drift, no binary re-upload, no loss of newer main docs. Original source Build ID remains `a922e9bdba4e57a4`. See [source-in-main audit](docs/integration/M3H_SOURCE_STACK_AUDIT_2026-10-24.md). +The [24 October 2026 M3.H release](https://github.com/masarray/arstack61850/releases/tag/processbus-sim-m3h-20261024) recorded 163,048 consecutive SV packets over 40.76 seconds, at 3,999.884 frames/s, with `smpSynch=2` across the captured interval and no missing or malformed SV frames. That result is a measured laboratory milestone, not a claim about later builds or external device acceptance. See the [test record](docs/releases/PROCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md). +**Release downloads always resolve through [GitHub's latest stable release](https://github.com/masarray/arstack61850/releases/latest)** rather than a hard-coded version. Release notes identify the firmware/software pairing and validated features for each build. -For electrical safety, transmit PTP and SV only on an isolated lab switch/VLAN: never introduce an unsolicited PTP grandmaster or SV publisher onto an operating substation LAN. +--- -### Earlier general-purpose Studio releases +## Developer and technical documentation -[v0.1.1](https://github.com/masarray/arstack61850/releases/tag/v0.1.1) remains an earlier Studio release; [v0.1.0](https://github.com/masarray/arstack61850/releases/tag/v0.1.0) retains historical 4I+4V / 4000 fps **`smpSynch=0`** evidence, 10/10 START/STOP and a 3600-second retained run. These older results are not the M3.H measured PTP/SV evidence. +The portable C++20 stack originated from the ARIEC61850 behavioral model. The engineering north star is deterministic IEC 61850 process-bus instrumentation with bounded runtime behavior, protocol evidence and platform-specific hardware timing. ## Start here diff --git a/apps/arstack_studio/README.md b/apps/arstack_studio/README.md index 0d0a743c7..f59ca4e2d 100644 --- a/apps/arstack_studio/README.md +++ b/apps/arstack_studio/README.md @@ -1,14 +1,14 @@ -# ARStack Studio — 4I + 4V SMV Injector / Generator +# ARStack Studio — Desktop IEC 61850 Lab Tool -ARStack Studio is the **canonical native desktop operator surface** for the first ARStack61850 public Sampled Values release. It is a Qt 6 / C++ / QML application; the ESP32-P4 remains the deterministic real-time publisher. +ARStack Studio is the native desktop interface for IEC 61850 Sampled Values experiments and ESP32-P4 process-bus simulation. -> **Newest public process-bus simulator:** [M3.H Lab Edition · 2026-10-24](https://github.com/masarray/arstack61850/releases/tag/processbus-sim-m3h-20261024) (Public Stable · engineering lab simulator). Earlier standalone Studio release: [v0.1.1](https://github.com/masarray/arstack61850/releases/tag/v0.1.1); historical P0 v0.1.0 is unchanged. -> -> **Source mainline:** Released M3.H firmware/Studio source and GitHub Release tag are both ancestors of `main` through [merged PR #299](https://github.com/masarray/arstack61850/pull/299), with 14/14 candidate CI SUCCESS and original `a922e9bdba4e57a4` hardware Build ID preserved in the published firmware. -> -> Install M3.H Setup **and flash its matched ESP32-P4 firmware** (build `a922e9bdba4e57a4`) to use the new PTP SOURCE continuity and simulated Global GM. The older firmware does not gain those features by updating Windows Studio alone. -> -> [Windows Setup EXE](https://github.com/masarray/arstack61850/releases/download/processbus-sim-m3h-20261024/ARStack-ProcessBus-M3H-win-x64-setup.exe) · [Portable](https://github.com/masarray/arstack61850/releases/download/processbus-sim-m3h-20261024/ARStack-ProcessBus-M3H-win-x64-portable.zip) · [firmware BIN](https://github.com/masarray/arstack61850/releases/download/processbus-sim-m3h-20261024/ARStack-ESP32P4-ProcessBus-M3H-firmware.bin) +**[Get the latest release](https://github.com/masarray/arstack61850/releases/latest)** · **[How to install](../../docs/INSTALL.md)** · **[Product website](https://masarray.github.io/arstack61850/)** + +On Windows, choose **Setup EXE** or **Portable ZIP** from the release page. For Linux, use an **AppImage** when it appears among the verified assets. For Ethernet output, connect the ESP32-P4 board and load its firmware from the **same release**. + +Start with a 4I+4V example, choose the timing mode, adjust voltage/current signals, and press **Start**. This software is made for engineers, students and lab experimenters; a GPS reference or protection relay is not needed just to try it. + +The M3.H release recorded verified 4 kHz SV and local simulated PTP traffic. These measurements belong to that specific release; newer builds have their own release notes and evidence. ## M3.H laboratory SV + PTP mode diff --git a/docs/INSTALL.md b/docs/INSTALL.md new file mode 100644 index 000000000..cebd10344 --- /dev/null +++ b/docs/INSTALL.md @@ -0,0 +1,49 @@ +# Install ARStack Studio + +**[Latest stable release](https://github.com/masarray/arstack61850/releases/latest)** · **[Product page](https://masarray.github.io/arstack61850/)** + +## Windows 10/11 (x64) + +Download the `win-x64-setup.exe` asset from **Latest stable release**, open it and follow Setup. The portable ZIP works without installing. + +To download the newest installer through PowerShell, verify its SHA-256 and open Setup automatically: + +```powershell +$script = Join-Path $env:TEMP 'arstack-install.ps1' +Invoke-WebRequest 'https://raw.githubusercontent.com/masarray/arstack61850/main/scripts/install-windows.ps1' -OutFile $script +& $script +``` + +To download a verified copy without starting the installer, run `& $script -DownloadOnly`. + +## Linux (x86_64) + +When the latest GitHub release offers `ARStack-Studio-linux-x86_64.AppImage`, download it, mark it executable (`chmod +x`) and open it. You can also install it into your home folder with: + +```bash +curl -fLsS https://raw.githubusercontent.com/masarray/arstack61850/main/scripts/install-linux.sh -o /tmp/arstack-install-linux.sh +bash /tmp/arstack-install-linux.sh +``` + +The install script checks the matching published SHA-256. It will explain when the current release does not yet contain a Linux build rather than downloading an older or unverified binary. The AppImage build is only published after Linux CI succeeds. + +## Windows Package Manager / WinGet + +WinGet uses an indexed, versioned package manifest. The release tooling prepares a manifest from the actual installer and SHA-256 once a release is final. The Microsoft community repository must accept the package **before** a public `winget install MasArray.ARStackStudio` command works; until then, use the Windows command above. A script or README alone does not register a WinGet package. + +Maintainer command (requires a tagged release with a unique Windows package version): + +```bash +python3 scripts/generate_winget_manifest.py --version X.Y.Z --installer-type inno +``` + +Review the generated YAML, validate with WinGet tooling, and submit it to [microsoft/winget-pkgs](https://github.com/microsoft/winget-pkgs). Use `--installer-type nullsoft` for the earlier NSIS installer format. + +## First lab experiment + +1. Open **ARStack Studio** and start from the built-in 4I+4V profile. +2. To publish actual Ethernet Sampled Values, connect a compatible ESP32-P4 and install the **matched firmware included with that release**. +3. Select a simulated PTP source, set waveform values, and press **Start**. +4. Inspect SV and PTP traffic on a dedicated test switch or network segment. + +No external timing reference is required for the basic simulation. Use the [release notes](https://github.com/masarray/arstack61850/releases/latest) for the exact validated capabilities and device/firmware pairing. diff --git a/docs/releases/PROCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md b/docs/releases/PROCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md index d120a4904..e30165105 100644 --- a/docs/releases/PROCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md +++ b/docs/releases/PROCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md @@ -2,7 +2,7 @@ **Public stable engineering lab simulator release:** ESP32-P4 Ethernet Sampled Values publisher + IEC/IEEE 61850-9-3-style internal PTP grandmaster, operated through ARStack Studio on Windows. -This release is intended for isolated process-bus experiments, engineering education, SV/PTP decoder testing, and preparation for interoperability testing with protection relays. It does **not** require an external GPS, Omicron, or relay to operate. It deliberately advertises the **simulated global grandmaster profile** for laboratory purposes. This is a simulation capability, not independently verified UTC accuracy. +This release is intended for isolated process-bus experiments, engineering education, SV/PTP decoder testing, and preparation for interoperability testing with protection relays. It does **not** require an external GPS or relay to operate. It deliberately advertises the **simulated global grandmaster profile** for laboratory purposes. This is a simulation capability, not independently verified UTC accuracy. ## What works and what was measured diff --git a/scripts/check_product_naming.py b/scripts/check_product_naming.py index a224a7e59..bd1506fb4 100644 --- a/scripts/check_product_naming.py +++ b/scripts/check_product_naming.py @@ -1,30 +1,44 @@ #!/usr/bin/env python3 -"""Fail CI when a prohibited product name enters tracked source or paths.""" +"""Keep public product copy vendor-neutral and prevent legacy competitor naming in code.""" from pathlib import Path import subprocess import sys ROOT = Path(__file__).resolve().parents[1] -PROHIBITED = ("ied" + "scout").encode("ascii") -tracked = subprocess.run(["git", "ls-files", "-z"], cwd=ROOT, - capture_output=True, check=True).stdout.split(b"\0") +# Split literals so this policy file does not itself introduce indexed brand mentions. +PROHIBITED = ( + "ied" + "scout", + "omi" + "cron", + "dan" + "eo", + "sipro" + "tec", +) +PUBLIC_PATHS = ( + "README.md", + "apps/arstack_studio/README.md", + "apps/ied_simulator/README.md", + "docs/INSTALL.md", + "docs/releases/PROCESS_BUS_SIMULATOR_M3H_PUBLIC_20261024.md", +) +tracked = subprocess.run( + ["git", "ls-files", "-z"], cwd=ROOT, capture_output=True, check=True +).stdout.split(b"\0") violations = [] for item in tracked: if not item: continue relative = item.decode("utf-8", errors="surrogateescape") - if PROHIBITED in item.lower(): + lower_name = relative.casefold() + if PROHIBITED[0] in lower_name: violations.append("filename: " + relative) + if relative not in PUBLIC_PATHS: + continue path = ROOT / relative if not path.is_file(): continue - with path.open("rb") as handle: - prefix = handle.read(4096) - if b"\0" in prefix: - continue - content = prefix + handle.read() - if PROHIBITED in content.lower(): - violations.append("text: " + relative) + data = path.read_text(encoding="utf-8", errors="replace").casefold() + for term in PROHIBITED: + if term in data: + violations.append(f"public copy: {relative} contains restricted term") if violations: sys.stderr.write("REPOSITORY_NAMING_GUARD_FAIL\n" + "\n".join(violations) + "\n") raise SystemExit(1) diff --git a/scripts/generate_winget_manifest.py b/scripts/generate_winget_manifest.py new file mode 100644 index 000000000..2b5850925 --- /dev/null +++ b/scripts/generate_winget_manifest.py @@ -0,0 +1,85 @@ +#!/usr/bin/env python3 +"""Prepare WinGet manifests from immutable GitHub Release metadata (not an upstream submission).""" +import argparse +import hashlib +import json +from pathlib import Path +import re +from urllib.request import Request, urlopen + +REPO="masarray/arstack61850" +PKG="MasArray.ARStackStudio" +def fetch_json(url): + with urlopen(Request(url,headers={"User-Agent":"arstack-winget-manifest", + "Accept":"application/vnd.github+json"}),timeout=30) as rsp: + return json.load(rsp) +def fetch_bytes(url): + with urlopen(Request(url,headers={"User-Agent":"arstack-winget-manifest"}),timeout=90) as rsp: + return rsp.read() +def main(): + p=argparse.ArgumentParser() + p.add_argument("--version",required=True,help="Unique WinGet SemVer release version, e.g. 0.2.0") + p.add_argument("--tag",help="GitHub release tag; default newest stable") + p.add_argument("--installer-type",choices=("inno","nullsoft"),required=True) + p.add_argument("--out",default="dist/winget") + args=p.parse_args() + if not re.fullmatch(r"\d+\.\d+\.\d+(?:\.\d+)?",args.version): + p.error("--version must be numeric X.Y.Z or X.Y.Z.W") + path=("tags/"+args.tag) if args.tag else "latest" + r=fetch_json(f"https://api.github.com/repos/{REPO}/releases/{path}") + if r.get("draft") or r.get("prerelease"): + p.error("Only a published stable release can be packaged for WinGet") + matches=[a for a in r.get("assets",[]) if re.search(r"(win|windows).*setup\.exe$",a["name"],re.I)] + if len(matches)!=1: + p.error("Expected exactly one Windows Setup installer in the release") + asset=matches[0] + digest=asset.get("digest") or "" + if re.fullmatch(r"sha256:[0-9a-fA-F]{64}",digest): + sha=digest.split(":")[1].upper() + else: + sums=next((a for a in r["assets"] if a["name"]=="SHA256SUMS.txt"),None) + if not sums: + p.error("Published installer checksum missing") + content=fetch_bytes(sums["browser_download_url"]).decode("ascii") + found=re.search(r"^([0-9a-fA-F]{64})\s+\*?"+re.escape(asset["name"])+r"\s*$",content,re.M) + if not found: + p.error("Installer entry not found in release SHA256SUMS.txt") + sha=found.group(1).upper() + # The upstream WinGet CI downloads and validates InstallerSha256 independently. + out=Path(args.out)/args.version + out.mkdir(parents=True,exist_ok=True) + installer=f"""# yaml-language-server: $schema=https://aka.ms/winget-manifest.installer.1.9.0.schema.json +PackageIdentifier: {PKG} +PackageVersion: {args.version} +InstallerType: {args.installer_type} +Installers: + - Architecture: x64 + InstallerUrl: {asset['browser_download_url']} + InstallerSha256: {sha} +ManifestType: installer +ManifestVersion: 1.9.0 +""" + version=f"""# yaml-language-server: $schema=https://aka.ms/winget-manifest.version.1.9.0.schema.json +PackageIdentifier: {PKG} +PackageVersion: {args.version} +DefaultLocale: en-US +ManifestType: version +ManifestVersion: 1.9.0 +""" + locale=f"""# yaml-language-server: $schema=https://aka.ms/winget-manifest.defaultLocale.1.9.0.schema.json +PackageIdentifier: {PKG} +PackageVersion: {args.version} +PackageLocale: en-US +Publisher: MasArray +PackageName: ARStack Studio +License: GPL-3.0-only +ShortDescription: Open-source IEC 61850 process-bus engineering laboratory +PackageUrl: https://github.com/{REPO} +ManifestType: defaultLocale +ManifestVersion: 1.9.0 +""" + for suffix,content in (("installer",installer),("version",version),("locale.en-US",locale)): + (out/f"{PKG}.{suffix}.yaml").write_text(content,encoding="utf-8") + print(f"WinGet manifests for {r['tag_name']}: {out}; review and submit upstream before advertising winget install") +if __name__=="__main__": + main() diff --git a/scripts/install-linux.sh b/scripts/install-linux.sh new file mode 100644 index 000000000..0b69e3db5 --- /dev/null +++ b/scripts/install-linux.sh @@ -0,0 +1,43 @@ +#!/usr/bin/env bash +# Download and install the verified Linux AppImage from the latest stable release. +set -euo pipefail +repo="masarray/arstack61850" +api="https://api.github.com/repos/$repo/releases/latest" +tmp="$(mktemp -d)" +trap 'rm -rf "$tmp"' EXIT +command -v curl >/dev/null || { echo 'Please install curl first.' >&2; exit 1; } +command -v python3 >/dev/null || { echo 'Please install Python 3 first.' >&2; exit 1; } +curl --fail --location --silent --show-error -H 'Accept: application/vnd.github+json' "$api" -o "$tmp/release.json" +readarray -t urls < <(python3 - "$tmp/release.json" <<'PY' +import json, sys +r=json.load(open(sys.argv[1])) +if r.get('draft') or r.get('prerelease'): + raise SystemExit('Latest release is not public stable.') +a={i['name']:i for i in r.get('assets',[])} +name='ARStack-Studio-linux-x86_64.AppImage' +checksum='ARStack-Studio-linux-x86_64.sha256' +if name not in a or checksum not in a: + raise SystemExit('Linux AppImage is not published for the latest release yet. Check GitHub Releases.') +print(a[name]['browser_download_url']) +print(a[checksum]['browser_download_url']) +PY +) +if (( ${#urls[@]} != 2 )); then exit 1; fi +curl -fLsS "${urls[0]}" -o "$tmp/ARStack-Studio-linux-x86_64.AppImage" +curl -fLsS "${urls[1]}" -o "$tmp/ARStack-Studio-linux-x86_64.sha256" +(cd "$tmp" && sha256sum -c ARStack-Studio-linux-x86_64.sha256) +opt="$HOME/.local/opt/arstack-studio" +bin="$HOME/.local/bin" +mkdir -p "$opt" "$bin" "$HOME/.local/share/applications" +install -m755 "$tmp/ARStack-Studio-linux-x86_64.AppImage" "$opt/ARStackStudio.AppImage" +ln -sfn "$opt/ARStackStudio.AppImage" "$bin/arstack-studio" +cat > "$HOME/.local/share/applications/arstack-studio.desktop" < Date: Sat, 24 Oct 2026 15:57:59 +0700 Subject: [PATCH 2/4] fix(ci): separate Linux release upload into permissions-scoped gated job --- .../arstack-studio-linux-appimage.yml | 25 ++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/.github/workflows/arstack-studio-linux-appimage.yml b/.github/workflows/arstack-studio-linux-appimage.yml index 1c7c094a8..3249ed315 100644 --- a/.github/workflows/arstack-studio-linux-appimage.yml +++ b/.github/workflows/arstack-studio-linux-appimage.yml @@ -22,7 +22,7 @@ jobs: runs-on: ubuntu-22.04 timeout-minutes: 55 permissions: - contents: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' && 'write' || 'read' }} + contents: read steps: - uses: actions/checkout@v4 with: @@ -145,13 +145,32 @@ jobs: if-no-files-found: error retention-days: 14 - - name: Add Linux artifacts to matching existing stable release - if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' }} + attach-verified-release: + name: Attach tested Linux package to exact release tag + needs: appimage + if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_tag != '' }} + runs-on: ubuntu-latest + permissions: + contents: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ inputs.release_tag }} + fetch-depth: 0 + - name: Download Linux build that passed the AppImage checks + uses: actions/download-artifact@v4 + with: + name: arstack-studio-linux-x86_64 + - name: Verify tag and release before publishing env: GH_TOKEN: ${{ github.token }} TAG: ${{ inputs.release_tag }} run: | set -euo pipefail + test "$(git rev-parse HEAD)" = "$(git rev-parse "$TAG^{commit}")" + gh release view "$TAG" --json isDraft,isPrerelease -q 'select(.isDraft==false and .isPrerelease==false)' > release-check.json + test -s release-check.json + sha256sum -c ARStack-Studio-linux-x86_64.sha256 gh release upload "$TAG" \ ARStack-Studio-linux-x86_64.AppImage \ ARStack-Studio-linux-x86_64.sha256 From b9eca0ae2476de98d1a3645d6d7e5cd340299e51 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 16:00:01 +0700 Subject: [PATCH 3/4] fix(linux): use supported Qt 6.10 linux_gcc_64 kit and matching AppImage icon --- .github/workflows/arstack-studio-linux-appimage.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/arstack-studio-linux-appimage.yml b/.github/workflows/arstack-studio-linux-appimage.yml index 3249ed315..5a9004bb5 100644 --- a/.github/workflows/arstack-studio-linux-appimage.yml +++ b/.github/workflows/arstack-studio-linux-appimage.yml @@ -52,7 +52,7 @@ jobs: uses: jurplel/install-qt-action@v4 with: version: '6.10.3' - arch: gcc_64 + arch: linux_gcc_64 modules: 'qtserialport' cache: true @@ -124,7 +124,7 @@ jobs: --appdir AppDir \ --executable AppDir/usr/bin/ARStackStudio \ --desktop-file arstack-studio.desktop \ - --icon-file assets/studio_icon/android-chrome-512x512.png \ + --icon-file AppDir/usr/share/icons/hicolor/512x512/apps/arstack-studio.png \ --plugin qt --output appimage shopt -s nullglob images=( ./*.AppImage ) From 746126fe8cc8adb51f5fdae026b85897530c21a4 Mon Sep 17 00:00:00 2001 From: Ari Sulistiono Date: Sat, 24 Oct 2026 16:02:23 +0700 Subject: [PATCH 4/4] ci(winget): dispatchable verified manifest generation and review artifact --- .github/workflows/arstack-winget-manifest.yml | 68 +++++++++++++++++++ 1 file changed, 68 insertions(+) create mode 100644 .github/workflows/arstack-winget-manifest.yml diff --git a/.github/workflows/arstack-winget-manifest.yml b/.github/workflows/arstack-winget-manifest.yml new file mode 100644 index 000000000..285317450 --- /dev/null +++ b/.github/workflows/arstack-winget-manifest.yml @@ -0,0 +1,68 @@ +name: ARStack Studio WinGet Manifest + +on: + pull_request: + paths: + - 'scripts/generate_winget_manifest.py' + - 'docs/INSTALL.md' + - '.github/workflows/arstack-winget-manifest.yml' + workflow_dispatch: + inputs: + package_version: + description: 'Unique WinGet package version, e.g. 0.2.0' + required: true + type: string + release_tag: + description: 'Published and verified GitHub stable release tag' + required: true + type: string + installer_type: + description: 'Match the actual installer technology for that release' + required: true + type: choice + options: + - inno + - nullsoft + +permissions: + contents: read + +jobs: + validate-generator: + name: Validate WinGet manifest generation tool + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - uses: actions/checkout@v4 + - name: Check Python generator + run: | + python3 -m py_compile scripts/generate_winget_manifest.py + python3 scripts/generate_winget_manifest.py --help + + prepare-manifest: + name: Generate exact verified installer manifests + needs: validate-generator + if: ${{ github.event_name == 'workflow_dispatch' }} + runs-on: ubuntu-latest + timeout-minutes: 10 + steps: + - uses: actions/checkout@v4 + - name: Generate manifest using published installer SHA-256 + env: + PACKAGE_VERSION: ${{ inputs.package_version }} + RELEASE_TAG: ${{ inputs.release_tag }} + INSTALLER_TYPE: ${{ inputs.installer_type }} + run: | + set -euo pipefail + python3 scripts/generate_winget_manifest.py \ + --version "$PACKAGE_VERSION" \ + --tag "$RELEASE_TAG" \ + --installer-type "$INSTALLER_TYPE" + grep -R 'InstallerSha256: [0-9A-F]\{64\}' dist/winget + + - uses: actions/upload-artifact@v4 + with: + name: arstack-studio-winget-manifests + path: dist/winget/ + if-no-files-found: error + retention-days: 30