From d71089ea5be716bf94127c263ff97ad7aefb331d Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:03:14 +0700 Subject: [PATCH 01/20] Add stable release trust notes --- landing/release-notes.json | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 landing/release-notes.json diff --git a/landing/release-notes.json b/landing/release-notes.json new file mode 100644 index 000000000..1e1654a01 --- /dev/null +++ b/landing/release-notes.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "product": "ARSAS", + "version": "1.6.17", + "title": "Verified updates and safer release delivery", + "summary": "ARSAS 1.6.17 adds a verified stable-update workflow, clearer user-controlled update handling, and reliability fixes around IED-card protocol actions and fault-record access.", + "highlights": [ + "Verified in-app update checks use the official ARSAS manifest and accept only the official stable installer URL.", + "Downloaded installers are checked against the published byte size and SHA-256 before Windows is allowed to open them.", + "The update prompt shows download progress, supports a 24-hour defer action, and keeps installation under user control.", + "Release publication uses the exact installer and portable artifacts that passed CI hash verification and installer smoke testing." + ], + "improvements": [ + "Protocol capability actions were moved to a dedicated IED-card row so lifecycle controls remain visible.", + "Capability-state refreshes are marshalled safely to the WPF UI thread when discovery state changes in the background.", + "Installer, portable ZIP and a combined SHA-256 file are published together through the stable release channel.", + "The public updater manifest is refreshed only after verified publication evidence has been recorded." + ], + "knownLimitations": [ + "Windows x64 is the only packaged desktop platform in the current stable release.", + "The public binaries are not Authenticode code-signed; Windows SmartScreen may show an unrecognized-publisher warning.", + "Raw-Ethernet GOOSE and Sampled Values workflows require Npcap, an approved adapter and suitable capture permissions.", + "Relay file-service directories and COMTRADE companion sets vary by vendor; a successful transfer does not prove record completeness or scaling correctness.", + "Active control and report-configuration writes still require approved procedures, plant authority and independent verification.", + "ARSAS is an engineering evidence tool and does not provide IEC 61850 conformance certification or replace calibrated protection testing." + ], + "codeSigning": { + "status": "unsigned", + "label": "Not Authenticode-signed", + "detail": "The current public Windows installer and portable binaries do not carry a commercial Authenticode publisher signature. Verify the published SHA-256 value before use. SmartScreen warnings are therefore possible and are not hidden from users." + }, + "screenshot": { + "src": "assets/screenshots/arsas-live-values.webp", + "width": 1507, + "height": 893, + "alt": "ARSAS 1.6.17 stable live-value workspace with IED, value, quality and timestamp evidence", + "caption": "Representative ARSAS 1.6.17 stable workspace. Device identity, value, quality, timestamp and acquisition context remain visible together." + }, + "issuesUrl": "https://github.com/masarray/arsas/issues/new/choose", + "releaseUrl": "https://github.com/masarray/arsas/releases/tag/v1.6.17" +} From 2d1f6c2012ae6300d5c6735a902e01f6b52c06b3 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:03:30 +0700 Subject: [PATCH 02/20] Expand stable release evidence --- landing/latest.json | 26 ++++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/landing/latest.json b/landing/latest.json index da86b4aea..cc617e15b 100644 --- a/landing/latest.json +++ b/landing/latest.json @@ -1,13 +1,31 @@ { "schemaVersion": 1, "product": "ARSAS", - "version": "1.6.16", + "version": "1.6.17", + "tag": "v1.6.17", "channel": "stable", - "publishedAtUtc": "2026-07-18T05:23:25.543679+00:00", + "publishedAtUtc": "2026-07-18T12:40:20.769158+00:00", + "sourceCommit": "1048caec850ad4dfa253e9bddb49a3b0776780be", + "releaseUrl": "https://github.com/masarray/arsas/releases/tag/v1.6.17", "installer": { "name": "ARSAS-Windows-x64-Setup.exe", "url": "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-Setup.exe", - "sha256": "9343ed05939ec8c9d253c1079e39429ed3dcf15db9f40982cc5b9b93335590d6", - "sizeBytes": 53372154 + "sha256": "7ead29100eee9e2808b6e941683151f56cc3aea5e5b579cbd5228035f813b4b3", + "sizeBytes": 53387744 + }, + "portable": { + "name": "ARSAS-Windows-x64-Portable.zip", + "url": "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-Portable.zip", + "sha256": "40e3ad425df50fc877e10e5104abfe84b382324caa458361f3e99352ee96494a", + "sizeBytes": 72559212 + }, + "checksums": { + "name": "ARSAS-Windows-x64-SHA256SUMS.txt", + "url": "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-SHA256SUMS.txt" + }, + "codeSigning": { + "status": "unsigned", + "platform": "Authenticode", + "detail": "The current public Windows binaries are not Authenticode code-signed. Windows SmartScreen may show an unrecognized-publisher warning; verify SHA-256 before use." } } From 5735014f319170889cb89d338eec5562d1c57f46 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:04:16 +0700 Subject: [PATCH 03/20] Add stable release notes page --- landing/templates/release-notes.html | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 landing/templates/release-notes.html diff --git a/landing/templates/release-notes.html b/landing/templates/release-notes.html new file mode 100644 index 000000000..f258cc6e3 --- /dev/null +++ b/landing/templates/release-notes.html @@ -0,0 +1,52 @@ + + + + + + ARSAS {{STABLE_VERSION}} Release Notes and Known Limitations + + + + + + + + + + + + + + + + + + + + + + + + + + {{> header}} +
+
Latest stable · Windows x64 · published {{STABLE_PUBLISHED_DATE}}

ARSAS {{STABLE_VERSION}}: {{RELEASE_TITLE}}

{{RELEASE_SUMMARY}}

Verified stable publication · exact CI artifacts · SHA-256 available
+ +
Version{{STABLE_VERSION}}
Published{{STABLE_PUBLISHED_DATE}}
Installer{{INSTALLER_SIZE}}
Portable ZIP{{PORTABLE_SIZE}}
+ +
What’s new

Changes in the latest stable release

    {{RELEASE_HIGHLIGHTS}}
Reliability improvements

Delivery and workflow hardening

    {{RELEASE_IMPROVEMENTS}}
+ +
Latest stable screenshot

Review the application before downloading.

The screenshot is representative of the published stable workspace and uses the same Windows product interface described in these release notes.

{{RELEASE_SCREENSHOT_ALT}}

{{STABLE_VERSION}}Stable Windows workspace

{{RELEASE_SCREENSHOT_CAPTION}}

+ +
Known limitations

Read these before field use.

    {{RELEASE_LIMITATIONS}}
Code-signing status

{{SIGNING_LABEL}}

{{SIGNING_DETAIL}}

Status: {{SIGNING_STATUS}}

A checksum verifies file identity against the published release evidence. It does not create a Windows publisher identity or replace Authenticode.

+ +
Found a release problem?

Report it with reproducible evidence.

Include the ARSAS version, package type, Windows version, exact error, relevant log or screenshot, IED/vendor context where appropriate, and whether the checksum matched.

+ + {{> download-cta}} +
+ {{> footer}} + + From be7a283fa94f8848804456892ede815bb0ea174f Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:04:50 +0700 Subject: [PATCH 04/20] Add Indonesian release notes page --- landing/templates/catatan-rilis.html | 52 ++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 landing/templates/catatan-rilis.html diff --git a/landing/templates/catatan-rilis.html b/landing/templates/catatan-rilis.html new file mode 100644 index 000000000..80904a78d --- /dev/null +++ b/landing/templates/catatan-rilis.html @@ -0,0 +1,52 @@ + + + + + + Catatan Rilis ARSAS {{STABLE_VERSION}} dan Batasan + + + + + + + + + + + + + + + + + + + + + + + + + + {{> header}} +
+
Stable terbaru · Windows x64 · dipublikasikan {{STABLE_PUBLISHED_DATE_ID}}

ARSAS {{STABLE_VERSION}}: {{RELEASE_TITLE_ID}}

{{RELEASE_SUMMARY_ID}}

Publikasi stabil terverifikasi · artifact CI yang sama · SHA-256 tersedia
+ +
Versi{{STABLE_VERSION}}
Dipublikasikan{{STABLE_PUBLISHED_DATE_ID}}
Installer{{INSTALLER_SIZE}}
Portable ZIP{{PORTABLE_SIZE}}
+ +
Yang baru

Perubahan pada rilis stabil terbaru

    {{RELEASE_HIGHLIGHTS_ID}}
Peningkatan reliability

Penguatan delivery dan workflow

    {{RELEASE_IMPROVEMENTS_ID}}
+ +
Screenshot versi stabil

Lihat aplikasi sebelum mengunduh.

Screenshot ini merepresentasikan workspace stabil yang dipublikasikan dan menggunakan antarmuka Windows yang dijelaskan pada catatan rilis.

{{RELEASE_SCREENSHOT_ALT_ID}}

{{STABLE_VERSION}}Workspace Windows stabil

{{RELEASE_SCREENSHOT_CAPTION_ID}}

+ +
Known limitations

Baca sebelum dipakai di lapangan.

    {{RELEASE_LIMITATIONS_ID}}
Status code-signing

{{SIGNING_LABEL_ID}}

{{SIGNING_DETAIL_ID}}

Status: {{SIGNING_STATUS}}

Checksum memverifikasi identitas file terhadap evidence release yang dipublikasikan. Checksum tidak membuat identitas publisher Windows dan tidak menggantikan Authenticode.

+ +
Menemukan masalah release?

Laporkan dengan evidence yang bisa direproduksi.

Sertakan versi ARSAS, jenis paket, versi Windows, error persis, log atau screenshot terkait, konteks IED/vendor bila relevan, dan apakah checksum sesuai.

+ + {{> download-cta-id}} +
+ {{> footer}} + + From 189d852ac5fe6e759363cb0851184fb05aa92ff1 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:05:50 +0700 Subject: [PATCH 05/20] Strengthen download trust and conversion --- landing/templates/download.html | 43 +++++++++++++++------------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/landing/templates/download.html b/landing/templates/download.html index 7566fa638..e0c376f13 100644 --- a/landing/templates/download.html +++ b/landing/templates/download.html @@ -3,8 +3,8 @@ - Download ARSAS for Windows — IEC 61850 Testing Software - + Download ARSAS {{STABLE_VERSION}} for Windows — Verified Packages + @@ -14,43 +14,38 @@ + - - + + - - - - - - - - + + + + {{> header}}
-
Download Center · stable release · Windows x64

Download ARSAS {{ARSAS_VERSION}} for Windows.

The ARSAS Download Center provides the stable Windows installer, portable package, SHA-256 checksums, system requirements and release guidance for practical IEC 61850 engineering work.

Latest stable channel · ARSAS {{ARSAS_VERSION}}
+
Official Download Center · stable release · Windows x64

Download ARSAS {{STABLE_VERSION}} with verifiable release evidence.

Choose the installer for a normal engineering workstation or the portable ZIP for a controlled folder-based deployment. Both packages are published from verified CI artifacts and have public SHA-256 values.

Latest stable channel · ARSAS {{STABLE_VERSION}} · published
-
Portable

Portable package

Useful for approved test laptops, temporary evaluation or controlled environments where a traditional installation is not preferred.

ARSAS-Windows-x64-Portable.zip · latest stable release
Download portable ZIP
+
Folder-based

Portable ZIP

Best for approved test laptops, temporary evaluation or environments where installation is not preferred. Extract to a writable local folder and update by replacing the package.

File
ARSAS-Windows-x64-Portable.zip
Size
{{PORTABLE_SIZE}}
Published
{{STABLE_PUBLISHED_DATE}}
Signing
{{SIGNING_LABEL}}
Download portable ZIP
-
Release integrity

Verify the package

Download the published checksum file and retain it with the package when preparing laboratory, FAT, SAT or commissioning work.

System scope

Prepared for Windows engineering workstations

  • Windows 10 or Windows 11, x64
  • Self-contained application package
  • Npcap required for raw-Ethernet GOOSE and Sampled Values workflows
  • Approved laboratory or commissioning network
+
Installer vs Portable

Choose based on deployment—not on feature availability.

The IEC 61850 application scope is the same. The difference is how Windows stores, launches, removes and updates the package.

DecisionInstallerPortable ZIP
Best forPrimary engineering workstationControlled test folder or temporary evaluation
SetupGuided Windows installationExtract and run from a writable local folder
Start menu and uninstallIncludedNot registered with Windows Installer
Update pathVerified installer download and launchManual package replacement recommended
Administrator rightsDepends on selected install scope and policyNormally unnecessary when using an approved user-writable folder
-
Before active use

Download readiness is not switching authority.

Confirm the package integrity, network interface, IED scope, access permissions, test procedure and independent verification before using control, report configuration, file access or other active functions.

FreeNo account, subscription or license key
TransparentPublic source, maturity labels and known boundaries
VerifiablePublished checksums and stable release evidence
PracticalInstaller and portable routes for engineering use
+
Release integrity

Copy and verify SHA-256 before use.

The values below come from the verified stable publication evidence. The combined checksum file is also available for automated or archived verification.

Installer SHA-256{{INSTALLER_SHA256}}
Portable SHA-256{{PORTABLE_SHA256}}
Download SHA-256 file
PowerShell verificationGet-FileHash .\ARSAS-Windows-x64-Setup.exe -Algorithm SHA256
-
Need context first?

Review the application and current capability scope.

See live MMS discovery, Smart Reporting, GOOSE, fault-record transfer, selected-RCB SCL export, diagnostics and guarded control before installation.

+
Code-signing status

{{SIGNING_LABEL}}

{{SIGNING_DETAIL}}

Status: {{SIGNING_STATUS}}
+ +
Issue reporting

Report package or runtime problems with evidence.

Include version {{STABLE_VERSION}}, installer or portable package, Windows version, exact error, screenshot or log, and whether the published checksum matched.

System scope

Prepared for approved Windows engineering workstations.

  • Windows 10 or Windows 11, x64
  • Self-contained .NET application package
  • Npcap required for raw-Ethernet GOOSE and Sampled Values
  • Approved laboratory, FAT, SAT or commissioning network
  • Independent authority required for active controls and writes
+ +
Review before installing

See what changed in ARSAS {{STABLE_VERSION}}.

Read the latest screenshot, update-verification design, known limitations, code-signing status and issue-reporting guidance.

{{> footer}} From 754cd2c6ee7cbc3d10cb9eed3dcd36d534869c20 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:06:53 +0700 Subject: [PATCH 06/20] Perkuat kepercayaan halaman unduh --- landing/templates/unduh.html | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/landing/templates/unduh.html b/landing/templates/unduh.html index c85136026..15891d545 100644 --- a/landing/templates/unduh.html +++ b/landing/templates/unduh.html @@ -3,8 +3,8 @@ - Unduh ARSAS untuk Windows | IEC 61850 - + Unduh ARSAS {{STABLE_VERSION}} untuk Windows — Paket Terverifikasi + @@ -14,29 +14,38 @@ + + - - + + - - - + + + + {{> header}}
-
Stable channel · Windows · versi {{ARSAS_VERSION}}

Unduh ARSAS dari pusat distribusi resmi.

Pilih installer untuk penggunaan normal atau portable ZIP untuk evaluasi tanpa proses instalasi. Checksum SHA-256 tersedia agar file yang diterima dapat diverifikasi.

+
Pusat Unduhan Resmi · stable release · Windows x64

Unduh ARSAS {{STABLE_VERSION}} dengan evidence release yang dapat diverifikasi.

Pilih installer untuk workstation engineering normal atau portable ZIP untuk deployment berbasis folder yang terkontrol. Kedua paket berasal dari artifact CI yang diverifikasi dan memiliki SHA-256 publik.

Stable terbaru · ARSAS {{STABLE_VERSION}} · dipublikasikan
-
Direkomendasikan

Windows Installer

Gunakan installer untuk shortcut, integrasi aplikasi dan update manual yang lebih teratur.

  • Windows x64
  • Stable release channel
  • Nama file tetap untuk latest release
Tanpa instalasi

Portable ZIP

Ekstrak ke folder lokal dan jalankan aplikasi untuk evaluasi atau pekerjaan dengan deployment terbatas.

  • Tidak menulis program ke Windows Installer database
  • Folder dapat dipindahkan
  • Tetap gunakan lokasi yang memiliki write permission
+
Berbasis folder

Portable ZIP

Cocok untuk laptop pengujian, evaluasi sementara atau lingkungan yang tidak menginginkan instalasi. Ekstrak ke folder lokal yang writable dan update dengan mengganti paket.

File
ARSAS-Windows-x64-Portable.zip
Ukuran
{{PORTABLE_SIZE}}
Publish
{{STABLE_PUBLISHED_DATE_ID}}
Signing
{{SIGNING_LABEL_ID}}
Unduh portable ZIP
-
Verifikasi file

Periksa checksum sebelum digunakan di lingkungan engineering.

Checksum membantu memastikan package yang diunduh sama dengan file yang dipublikasikan pada release resmi.

SHA-256 checksums

Unduh daftar checksum resmi, lalu bandingkan dengan hasil hash lokal.

PowerShell

Get-FileHash .\ARSAS-Windows-x64-Setup.exe -Algorithm SHA256

Nilai hash harus identik dengan entry installer pada file checksum resmi.

+
Installer vs Portable

Pilih berdasarkan cara deployment, bukan perbedaan fitur.

Ruang lingkup aplikasi IEC 61850 sama. Perbedaannya adalah cara Windows menyimpan, menjalankan, menghapus dan memperbarui paket.

KeputusanInstallerPortable ZIP
Paling cocok untukWorkstation engineering utamaFolder pengujian terkontrol atau evaluasi sementara
SetupProses instalasi Windows terpanduEkstrak dan jalankan dari folder lokal writable
Start menu dan uninstallTersediaTidak terdaftar sebagai instalasi Windows
Jalur updateDownload dan launch installer terverifikasiPenggantian paket manual direkomendasikan
Hak administratorBergantung scope instalasi dan policyBiasanya tidak diperlukan pada folder user yang disetujui
-
Sebelum menjalankan

Gunakan dalam boundary yang disetujui.

Operating system

Gunakan Windows 10 atau Windows 11 x64 dengan hak akses yang sesuai untuk capture, file dan komunikasi yang dibutuhkan.

Network access

Pastikan endpoint, TCP port 102, adapter station bus dan firewall sudah diizinkan oleh project atau laboratory procedure.

Control safety

Jangan mengirim command ke primary equipment tanpa approved test boundary, interlock review dan personel yang bertanggung jawab.

Current scope

Baca capability, roadmap dan technical review policy untuk membedakan fitur available, conditional, preview dan roadmap.

+
Integritas release

Salin dan verifikasi SHA-256 sebelum digunakan.

Nilai berikut berasal dari evidence publikasi stable yang terverifikasi. File checksum gabungan juga tersedia untuk verifikasi otomatis atau arsip.

SHA-256 installer{{INSTALLER_SHA256}}
SHA-256 portable{{PORTABLE_SHA256}}
Unduh file SHA-256
Verifikasi PowerShellGet-FileHash .\ARSAS-Windows-x64-Setup.exe -Algorithm SHA256
+ +
Status code-signing

{{SIGNING_LABEL_ID}}

{{SIGNING_DETAIL_ID}}

Status: {{SIGNING_STATUS}}
+ +
Pelaporan issue

Laporkan masalah paket atau runtime dengan evidence.

Sertakan versi {{STABLE_VERSION}}, paket installer atau portable, versi Windows, error persis, screenshot atau log, dan apakah checksum sesuai.

Ruang lingkup sistem

Disiapkan untuk workstation Windows yang disetujui.

  • Windows 10 atau Windows 11, x64
  • Paket aplikasi .NET self-contained
  • Npcap diperlukan untuk raw-Ethernet GOOSE dan Sampled Values
  • Jaringan laboratorium, FAT, SAT atau commissioning yang disetujui
  • Otoritas independen untuk control aktif dan write operation
+ +
Tinjau sebelum instalasi

Lihat perubahan pada ARSAS {{STABLE_VERSION}}.

Baca screenshot stabil terbaru, desain verifikasi update, known limitations, status code-signing dan panduan pelaporan issue.

{{> download-cta-id}}
From 78fd7944bf3e7036fb24c2da7f7ac99f717fe4dd Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:07:11 +0700 Subject: [PATCH 07/20] Standardize English download CTA --- landing/partials/download-cta.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/landing/partials/download-cta.html b/landing/partials/download-cta.html index 3f978252d..89181a4ba 100644 --- a/landing/partials/download-cta.html +++ b/landing/partials/download-cta.html @@ -2,11 +2,12 @@
- Start with the product -

Download ARSAS {{ARSAS_VERSION}} for Windows.

-

Use the official installer for normal workstations, choose the portable ZIP for controlled test laptops, and verify the package with published SHA-256 checksums.

+ Verified stable release +

Download ARSAS {{STABLE_VERSION}} for Windows.

+

Use the installer for a normal workstation, choose the portable ZIP for a controlled folder deployment, and verify the published SHA-256 before use.

+ What’s new, limitations and signing status →
- +
From eefef44956edeb05893e9226c89b6acff2269c9e Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:07:25 +0700 Subject: [PATCH 08/20] Standardize Indonesian download CTA --- landing/partials/download-cta-id.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landing/partials/download-cta-id.html b/landing/partials/download-cta-id.html index 79698f9f6..b100ed048 100644 --- a/landing/partials/download-cta-id.html +++ b/landing/partials/download-cta-id.html @@ -1 +1 @@ -
Mulai dengan ARSAS

Unduh ARSAS {{ARSAS_VERSION}} untuk Windows.

Gunakan installer untuk workstation normal, pilih portable ZIP untuk laptop pengujian terkontrol, dan verifikasi paket menggunakan checksum SHA-256 yang dipublikasikan.

+
Release stabil terverifikasi

Unduh ARSAS {{STABLE_VERSION}} untuk Windows.

Gunakan installer untuk workstation normal, pilih portable ZIP untuk deployment berbasis folder yang terkontrol, dan verifikasi SHA-256 sebelum digunakan.

Yang baru, batasan dan status signing →
From 642001076aa99cd678a056f63449179c7a22bb0b Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:07:45 +0700 Subject: [PATCH 09/20] Expose release trust links in footer --- landing/partials/footer.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/landing/partials/footer.html b/landing/partials/footer.html index 843d4a4a0..ced79ccbe 100644 --- a/landing/partials/footer.html +++ b/landing/partials/footer.html @@ -7,7 +7,7 @@
@@ -19,7 +19,7 @@
From 93902261f3eb968030c6b702c275a5d81c31b76c Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:09:41 +0700 Subject: [PATCH 10/20] Localize stable release trust notes --- landing/release-notes.json | 30 ++++++++++++++++++++++++++++-- 1 file changed, 28 insertions(+), 2 deletions(-) diff --git a/landing/release-notes.json b/landing/release-notes.json index 1e1654a01..a1984ed11 100644 --- a/landing/release-notes.json +++ b/landing/release-notes.json @@ -3,19 +3,33 @@ "product": "ARSAS", "version": "1.6.17", "title": "Verified updates and safer release delivery", + "titleId": "Update terverifikasi dan distribusi release yang lebih aman", "summary": "ARSAS 1.6.17 adds a verified stable-update workflow, clearer user-controlled update handling, and reliability fixes around IED-card protocol actions and fault-record access.", + "summaryId": "ARSAS 1.6.17 menambahkan workflow update stabil yang terverifikasi, kontrol update yang tetap berada di tangan user, serta perbaikan reliability pada action protokol di IED card dan akses fault record.", "highlights": [ "Verified in-app update checks use the official ARSAS manifest and accept only the official stable installer URL.", "Downloaded installers are checked against the published byte size and SHA-256 before Windows is allowed to open them.", "The update prompt shows download progress, supports a 24-hour defer action, and keeps installation under user control.", "Release publication uses the exact installer and portable artifacts that passed CI hash verification and installer smoke testing." ], + "highlightsId": [ + "Pemeriksaan update di dalam aplikasi menggunakan manifest resmi ARSAS dan hanya menerima URL installer stable yang resmi.", + "Installer yang diunduh diperiksa terhadap ukuran byte dan SHA-256 yang dipublikasikan sebelum diizinkan dibuka oleh Windows.", + "Prompt update menampilkan progress download, menyediakan penundaan 24 jam, dan tetap menyerahkan keputusan instalasi kepada user.", + "Publikasi release menggunakan artifact installer dan portable yang sama persis dengan artifact yang lulus verifikasi hash CI dan installer smoke test." + ], "improvements": [ "Protocol capability actions were moved to a dedicated IED-card row so lifecycle controls remain visible.", "Capability-state refreshes are marshalled safely to the WPF UI thread when discovery state changes in the background.", "Installer, portable ZIP and a combined SHA-256 file are published together through the stable release channel.", "The public updater manifest is refreshed only after verified publication evidence has been recorded." ], + "improvementsId": [ + "Action capability protokol dipindahkan ke row khusus pada IED card agar lifecycle control tetap terlihat.", + "Refresh status capability dipindahkan secara aman ke WPF UI thread ketika status discovery berubah dari background worker.", + "Installer, portable ZIP dan file SHA-256 gabungan dipublikasikan bersama melalui stable release channel.", + "Manifest updater publik hanya diperbarui setelah evidence publikasi terverifikasi selesai direkam." + ], "knownLimitations": [ "Windows x64 is the only packaged desktop platform in the current stable release.", "The public binaries are not Authenticode code-signed; Windows SmartScreen may show an unrecognized-publisher warning.", @@ -24,17 +38,29 @@ "Active control and report-configuration writes still require approved procedures, plant authority and independent verification.", "ARSAS is an engineering evidence tool and does not provide IEC 61850 conformance certification or replace calibrated protection testing." ], + "knownLimitationsId": [ + "Windows x64 adalah satu-satunya platform desktop yang dipaketkan pada stable release saat ini.", + "Binary publik belum ditandatangani dengan Authenticode; Windows SmartScreen dapat menampilkan peringatan unrecognized publisher.", + "Workflow raw-Ethernet GOOSE dan Sampled Values memerlukan Npcap, adapter yang disetujui dan permission capture yang sesuai.", + "Struktur directory file service relay dan pasangan file COMTRADE berbeda antar-vendor; transfer berhasil tidak membuktikan record lengkap atau scaling sudah benar.", + "Active control dan write pada konfigurasi reporting tetap memerlukan procedure yang disetujui, plant authority dan independent verification.", + "ARSAS adalah tool engineering evidence dan tidak memberikan sertifikasi conformity IEC 61850 atau menggantikan calibrated protection testing." + ], "codeSigning": { "status": "unsigned", "label": "Not Authenticode-signed", - "detail": "The current public Windows installer and portable binaries do not carry a commercial Authenticode publisher signature. Verify the published SHA-256 value before use. SmartScreen warnings are therefore possible and are not hidden from users." + "labelId": "Belum ditandatangani dengan Authenticode", + "detail": "The current public Windows installer and portable binaries do not carry a commercial Authenticode publisher signature. Verify the published SHA-256 value before use. SmartScreen warnings are therefore possible and are not hidden from users.", + "detailId": "Installer Windows dan binary portable publik saat ini belum memiliki commercial Authenticode publisher signature. Verifikasi nilai SHA-256 yang dipublikasikan sebelum digunakan. Karena itu peringatan SmartScreen masih mungkin muncul dan status ini tidak disembunyikan dari user." }, "screenshot": { "src": "assets/screenshots/arsas-live-values.webp", "width": 1507, "height": 893, "alt": "ARSAS 1.6.17 stable live-value workspace with IED, value, quality and timestamp evidence", - "caption": "Representative ARSAS 1.6.17 stable workspace. Device identity, value, quality, timestamp and acquisition context remain visible together." + "altId": "Workspace live value ARSAS 1.6.17 stable dengan evidence IED, value, quality dan timestamp", + "caption": "Representative ARSAS 1.6.17 stable workspace. Device identity, value, quality, timestamp and acquisition context remain visible together.", + "captionId": "Representasi workspace stable ARSAS 1.6.17. Identitas device, value, quality, timestamp dan konteks acquisition tetap terlihat bersama." }, "issuesUrl": "https://github.com/masarray/arsas/issues/new/choose", "releaseUrl": "https://github.com/masarray/arsas/releases/tag/v1.6.17" From 78cfe0acb60fcbd1fdd55d45ad11236cfe688292 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:11:14 +0700 Subject: [PATCH 11/20] Add checksum copy actions --- landing/app.js | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/landing/app.js b/landing/app.js index d21acf992..96c902975 100644 --- a/landing/app.js +++ b/landing/app.js @@ -44,4 +44,27 @@ document.querySelectorAll('[data-year]').forEach(node => { node.textContent = String(new Date().getFullYear()); }); + + document.querySelectorAll('[data-copy-value]').forEach(button => { + if (!(button instanceof HTMLButtonElement)) return; + const original = button.textContent || ''; + const copiedLabel = document.documentElement.lang === 'id' ? 'Tersalin' : 'Copied'; + const failedLabel = document.documentElement.lang === 'id' ? 'Salin manual' : 'Copy manually'; + let restoreTimer; + + button.addEventListener('click', async () => { + const value = button.dataset.copyValue || ''; + if (!value) return; + window.clearTimeout(restoreTimer); + try { + await navigator.clipboard.writeText(value); + button.textContent = copiedLabel; + } catch { + button.textContent = failedLabel; + } + restoreTimer = window.setTimeout(() => { + button.textContent = original; + }, 2200); + }); + }); })(); From 8da3ed7e13575241838a3c55ee681de68b4cd536 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:11:48 +0700 Subject: [PATCH 12/20] Style release trust and package verification --- landing/download.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landing/download.css b/landing/download.css index 30740d0af..f19557c58 100644 --- a/landing/download.css +++ b/landing/download.css @@ -1 +1 @@ -.download-hero{padding-bottom:2.5rem}.release-status{display:inline-flex;align-items:center;gap:.62rem;margin-top:1.35rem;padding:.62rem .78rem;border:1px solid rgba(56,189,248,.2);border-radius:13px;color:var(--muted);background:rgba(56,189,248,.055);font-size:.88rem}.download-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.download-card{display:flex;flex-direction:column;min-height:330px;padding:1.5rem;border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(180deg,rgba(255,255,255,.065),rgba(255,255,255,.032));box-shadow:0 18px 55px rgba(0,0,0,.18)}.download-card.recommended{border-color:rgba(56,189,248,.28);background:radial-gradient(circle at 10% 0%,rgba(56,189,248,.13),transparent 18rem),linear-gradient(180deg,rgba(59,130,246,.09),rgba(255,255,255,.03))}.download-card h2{margin-top:.65rem;font-size:clamp(1.75rem,3vw,2.45rem)}.download-card p{color:var(--muted)}.download-meta{margin-top:auto;padding:.85rem 0;color:var(--subtle);font-size:.86rem;overflow-wrap:anywhere}.download-action{width:100%}.download-action[aria-disabled="true"]{opacity:.62;pointer-events:none}.download-card .kicker{align-self:flex-start}@media(max-width:760px){.download-grid{grid-template-columns:1fr}.download-card{min-height:auto}} \ No newline at end of file +.download-hero{padding-bottom:2.5rem}.release-status{display:inline-flex;align-items:center;gap:.62rem;margin-top:1.35rem;padding:.62rem .78rem;border:1px solid rgba(56,189,248,.2);border-radius:13px;color:var(--muted);background:rgba(56,189,248,.055);font-size:.88rem}.download-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.download-card{display:flex;flex-direction:column;min-height:410px;padding:1.5rem;border:1px solid var(--line);border-radius:var(--radius);background:linear-gradient(180deg,rgba(255,255,255,.065),rgba(255,255,255,.032));box-shadow:0 18px 55px rgba(0,0,0,.18)}.download-card.recommended{border-color:rgba(56,189,248,.28);background:radial-gradient(circle at 10% 0%,rgba(56,189,248,.13),transparent 18rem),linear-gradient(180deg,rgba(59,130,246,.09),rgba(255,255,255,.03))}.download-card h2{margin-top:.65rem;font-size:clamp(1.75rem,3vw,2.45rem)}.download-card p{color:var(--muted)}.download-meta{margin-top:auto;padding:.85rem 0;color:var(--subtle);font-size:.86rem;overflow-wrap:anywhere}.download-action{width:100%;margin-top:auto}.download-action[aria-disabled="true"]{opacity:.62;pointer-events:none}.download-card .kicker{align-self:flex-start}.release-facts{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:.8rem}.release-fact{padding:1rem 1.05rem;border:1px solid var(--line);border-radius:15px;background:rgba(255,255,255,.035)}.release-fact span{display:block;color:var(--subtle);font-size:.78rem;text-transform:uppercase;letter-spacing:.06em}.release-fact strong{display:block;margin-top:.35rem;font-size:1.05rem}.package-meta{display:grid;gap:.48rem;margin:1.1rem 0 1.25rem;padding:1rem 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}.package-meta div{display:grid;grid-template-columns:5.5rem minmax(0,1fr);gap:.75rem}.package-meta dt{color:var(--subtle);font-size:.82rem}.package-meta dd{margin:0;color:var(--text);font-size:.85rem;overflow-wrap:anywhere}.package-comparison{overflow:hidden;border:1px solid var(--line);border-radius:var(--radius);background:rgba(255,255,255,.025)}.comparison-row{display:grid;grid-template-columns:minmax(8rem,.8fr) repeat(2,minmax(0,1.3fr));gap:1rem;padding:.92rem 1.05rem;border-top:1px solid var(--line)}.comparison-row:first-child{border-top:0}.comparison-row>span:first-child{color:var(--subtle);font-weight:680}.comparison-head{background:rgba(56,189,248,.075)}.checksum-grid{display:grid;grid-template-columns:repeat(2,minmax(0,1fr));gap:1rem}.checksum-card{padding:1.25rem;border:1px solid var(--line);border-radius:var(--radius-sm);background:rgba(255,255,255,.035)}.checksum-value{display:block;margin:.85rem 0;padding:.85rem;border:1px solid rgba(125,211,252,.16);background:rgba(3,10,20,.58);font-size:.82rem;line-height:1.55;overflow-wrap:anywhere;user-select:all}.copy-button{cursor:pointer}.verification-command{display:grid;grid-template-columns:auto minmax(0,1fr);align-items:center;gap:.9rem;margin-top:1rem;padding:1rem 1.05rem;border:1px solid var(--line);border-radius:14px;background:rgba(255,255,255,.025)}.verification-command span{color:var(--subtle);font-size:.83rem}.verification-command code{overflow-wrap:anywhere}.release-list{margin:.8rem 0 0;padding-left:1.15rem;color:var(--muted)}.release-list li+li{margin-top:.55rem}.warning-panel{border-color:rgba(251,191,36,.25)!important;background:linear-gradient(145deg,rgba(245,158,11,.07),rgba(255,255,255,.025))!important}.trust-badge{display:inline-flex;margin-top:1rem;padding:.48rem .68rem;border-radius:999px;font-size:.78rem;font-weight:750;letter-spacing:.025em}.trust-badge-warning{border:1px solid rgba(251,191,36,.3);color:#fde68a;background:rgba(245,158,11,.1)}.small-note{margin-top:1rem!important;color:var(--subtle)!important;font-size:.82rem}.text-link{display:inline-flex;margin-top:.7rem;color:#bae6fd;font-weight:650}.text-link:hover{text-decoration:underline}@media(max-width:900px){.release-facts{grid-template-columns:repeat(2,minmax(0,1fr))}}@media(max-width:760px){.download-grid,.checksum-grid{grid-template-columns:1fr}.download-card{min-height:auto}.comparison-row{grid-template-columns:1fr;gap:.35rem;padding:1rem}.comparison-head{display:none}.comparison-row>span:first-child{margin-bottom:.25rem}.comparison-row>span:nth-child(2):before{content:"Installer: ";font-weight:700;color:var(--text)}.comparison-row>span:nth-child(3):before{content:"Portable: ";font-weight:700;color:var(--text)}.verification-command{grid-template-columns:1fr}}@media(max-width:520px){.release-facts{grid-template-columns:1fr}.package-meta div{grid-template-columns:1fr;gap:.2rem}} From b6085feaeac06c4b9231f7f703c25f03284da0e7 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:12:54 +0700 Subject: [PATCH 13/20] Register release trust pages --- landing/site.json | 2 ++ 1 file changed, 2 insertions(+) diff --git a/landing/site.json b/landing/site.json index 9f837499a..86710bb71 100644 --- a/landing/site.json +++ b/landing/site.json @@ -27,6 +27,8 @@ {"path": "id.html", "template": "id.html", "language": "id", "contentType": "localized", "alternates": {"en": "", "id": "id.html", "x-default": ""}, "changefreq": "weekly", "priority": "0.97", "lastmod": "2026-07-20"}, {"path": "download.html", "template": "download.html", "language": "en", "alternates": {"en": "download.html", "id": "unduh.html", "x-default": "download.html"}, "changefreq": "weekly", "priority": "0.98", "lastmod": "2026-07-20"}, {"path": "unduh.html", "template": "unduh.html", "language": "id", "contentType": "localized", "alternates": {"en": "download.html", "id": "unduh.html", "x-default": "download.html"}, "changefreq": "weekly", "priority": "0.95", "lastmod": "2026-07-20"}, + {"path": "release-notes.html", "template": "release-notes.html", "language": "en", "contentType": "release", "alternates": {"en": "release-notes.html", "id": "catatan-rilis.html", "x-default": "release-notes.html"}, "changefreq": "weekly", "priority": "0.96", "lastmod": "2026-07-20"}, + {"path": "catatan-rilis.html", "template": "catatan-rilis.html", "language": "id", "contentType": "localized", "alternates": {"en": "release-notes.html", "id": "catatan-rilis.html", "x-default": "release-notes.html"}, "changefreq": "weekly", "priority": "0.93", "lastmod": "2026-07-20"}, {"path": "features.html", "template": "features.html", "changefreq": "monthly", "priority": "0.94", "lastmod": "2026-07-20"}, {"path": "solutions.html", "template": "solutions.html", "changefreq": "monthly", "priority": "0.96", "lastmod": "2026-07-20"}, {"path": "fat-testing.html", "template": "fat-testing.html", "language": "en", "alternates": {"en": "fat-testing.html", "id": "pengujian-fat-iec61850.html", "x-default": "fat-testing.html"}, "changefreq": "monthly", "priority": "0.94", "lastmod": "2026-07-20"}, From a3630a28450164c081b40efce8dec162231d1afb Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:14:43 +0700 Subject: [PATCH 14/20] Build website from verified release evidence --- scripts/build-product-site.py | 182 +++++++++++++++++++++++++++++++--- 1 file changed, 168 insertions(+), 14 deletions(-) diff --git a/scripts/build-product-site.py b/scripts/build-product-site.py index a66db3b29..543e49476 100644 --- a/scripts/build-product-site.py +++ b/scripts/build-product-site.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Build the ARSAS product website from templates, partials and product data.""" +"""Build the ARSAS product website from templates, product data and verified release evidence.""" from __future__ import annotations @@ -9,6 +9,7 @@ import shutil import struct import xml.etree.ElementTree as ET +from datetime import datetime from pathlib import Path from xml.sax.saxutils import escape @@ -17,11 +18,25 @@ TEMPLATES = SOURCE / "templates" PARTIALS = SOURCE / "partials" CONFIG_PATH = SOURCE / "site.json" +RELEASE_NOTES_PATH = SOURCE / "release-notes.json" +DEFAULT_RELEASE_EVIDENCE_PATH = SOURCE / "latest.json" PROJECT_PATH = ROOT / "ArIED61850Tester.csproj" APP_ICON_SOURCE = ROOT / "Assets" / "app-icon.png" INCLUDE_PATTERN = re.compile(r"\{\{>\s*([a-z0-9-]+)\s*\}\}", re.IGNORECASE) TOKEN_PATTERN = re.compile(r"\{\{([A-Z0-9_]+)\}\}") VERIFICATION_PATTERN = re.compile(r"google[a-z0-9]+\.html", re.IGNORECASE) +SEMVER_PATTERN = re.compile(r"\d+\.\d+\.\d+") +SHA256_PATTERN = re.compile(r"[0-9a-fA-F]{64}") + + +def read_json(path: Path, label: str) -> dict[str, object]: + try: + value = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + raise SystemExit(f"Cannot read {label}: {exc}") from exc + if not isinstance(value, dict): + raise SystemExit(f"{label} must contain a JSON object") + return value def png_size(path: Path) -> tuple[int, int]: @@ -46,16 +61,13 @@ def read_version() -> str: except (OSError, ET.ParseError) as exc: raise SystemExit(f"Cannot read ARSAS project version: {exc}") from exc version = (project.findtext(".//Version") or "").strip() - if not re.fullmatch(r"\d+\.\d+\.\d+", version): + if not SEMVER_PATTERN.fullmatch(version): raise SystemExit("ARSAS project Version must use major.minor.patch") return version def read_config() -> dict[str, object]: - try: - config = json.loads(CONFIG_PATH.read_text(encoding="utf-8")) - except (OSError, json.JSONDecodeError) as exc: - raise SystemExit(f"Cannot read landing/site.json: {exc}") from exc + config = read_json(CONFIG_PATH, "landing/site.json") required = ( "product.name", "product.canonicalRoot", "product.repository", "product.engineRepository", "author.name", "author.linkedin", "author.github", "downloads.installer", @@ -80,11 +92,108 @@ def read_config() -> dict[str, object]: return config -def token_values(config: dict[str, object], version: str) -> dict[str, str]: +def require_text(value: object, label: str) -> str: + if not isinstance(value, str) or not value.strip(): + raise SystemExit(f"Invalid release field: {label}") + return value.strip() + + +def require_list(value: object, label: str, minimum: int = 1) -> list[str]: + if not isinstance(value, list) or len(value) < minimum or not all(isinstance(item, str) and item.strip() for item in value): + raise SystemExit(f"Invalid release list: {label}") + return [item.strip() for item in value] + + +def read_release_data(path: Path) -> tuple[dict[str, object], dict[str, object]]: + evidence = read_json(path, f"stable release evidence {path}") + notes = read_json(RELEASE_NOTES_PATH, "landing/release-notes.json") + if evidence.get("schemaVersion") != 1 or evidence.get("product") != "ARSAS": + raise SystemExit("Stable release evidence has invalid identity") + version = require_text(evidence.get("version"), "version") + if not SEMVER_PATTERN.fullmatch(version) or notes.get("version") != version: + raise SystemExit("Release evidence and release notes versions must match") + if evidence.get("channel") != "stable": + raise SystemExit("Only stable release evidence may build the public Download Center") + require_text(evidence.get("publishedAtUtc"), "publishedAtUtc") + require_text(evidence.get("sourceCommit"), "sourceCommit") + for key, expected_name in ( + ("installer", "ARSAS-Windows-x64-Setup.exe"), + ("portable", "ARSAS-Windows-x64-Portable.zip"), + ): + package = evidence.get(key) + if not isinstance(package, dict) or package.get("name") != expected_name: + raise SystemExit(f"Stable release evidence has invalid {key} identity") + if not SHA256_PATTERN.fullmatch(str(package.get("sha256", ""))): + raise SystemExit(f"Stable release evidence has invalid {key} SHA-256") + if not 1_000_000 <= int(package.get("sizeBytes", 0)) <= 500_000_000: + raise SystemExit(f"Stable release evidence has invalid {key} size") + require_text(package.get("url"), f"{key}.url") + checksums = evidence.get("checksums") + if not isinstance(checksums, dict): + raise SystemExit("Stable release evidence is missing checksum asset") + require_text(checksums.get("url"), "checksums.url") + signing = evidence.get("codeSigning") + if not isinstance(signing, dict) or signing.get("status") not in {"signed", "unsigned"}: + raise SystemExit("Stable release evidence must declare code-signing status") + + for key in ("title", "titleId", "summary", "summaryId", "issuesUrl", "releaseUrl"): + require_text(notes.get(key), key) + for key in ("highlights", "highlightsId", "improvements", "improvementsId", "knownLimitations", "knownLimitationsId"): + require_list(notes.get(key), key, 4) + note_signing = notes.get("codeSigning") + if not isinstance(note_signing, dict) or note_signing.get("status") != signing.get("status"): + raise SystemExit("Release notes code-signing status must match release evidence") + for key in ("label", "labelId", "detail", "detailId"): + require_text(note_signing.get(key), f"codeSigning.{key}") + screenshot = notes.get("screenshot") + if not isinstance(screenshot, dict): + raise SystemExit("Release notes screenshot metadata is missing") + for key in ("src", "alt", "altId", "caption", "captionId"): + require_text(screenshot.get(key), f"screenshot.{key}") + if not (SOURCE / str(screenshot["src"])).exists(): + raise SystemExit("Release notes screenshot source does not exist") + return evidence, notes + + +def human_size(size_bytes: int) -> str: + return f"{size_bytes / (1024 * 1024):.1f} MiB" + + +def release_dates(value: str) -> tuple[str, str, str]: + try: + parsed = datetime.fromisoformat(value.replace("Z", "+00:00")) + except ValueError as exc: + raise SystemExit(f"Invalid stable release publish date: {value}") from exc + month_id = ( + "Januari", "Februari", "Maret", "April", "Mei", "Juni", + "Juli", "Agustus", "September", "Oktober", "November", "Desember", + ) + english = f"{parsed.day} {parsed.strftime('%B')} {parsed.year}" + indonesian = f"{parsed.day} {month_id[parsed.month - 1]} {parsed.year}" + return value, english, indonesian + + +def list_html(items: list[str]) -> str: + return "".join(f"
  • {escape(item)}
  • " for item in items) + + +def token_values( + config: dict[str, object], + version: str, + evidence: dict[str, object], + notes: dict[str, object], +) -> dict[str, str]: product = config["product"] author = config["author"] downloads = config["downloads"] + installer = evidence["installer"] + portable = evidence["portable"] + signing = notes["codeSigning"] + screenshot = notes["screenshot"] assert isinstance(product, dict) and isinstance(author, dict) and isinstance(downloads, dict) + assert isinstance(installer, dict) and isinstance(portable, dict) + assert isinstance(signing, dict) and isinstance(screenshot, dict) + published_iso, published_en, published_id = release_dates(str(evidence["publishedAtUtc"])) return { "ARSAS_VERSION": version, "PRODUCT_NAME": str(product["name"]), @@ -97,6 +206,38 @@ def token_values(config: dict[str, object], version: str) -> dict[str, str]: "INSTALLER_URL": str(downloads["installer"]), "PORTABLE_URL": str(downloads["portable"]), "CHECKSUMS_URL": str(downloads["checksums"]), + "STABLE_VERSION": str(evidence["version"]), + "STABLE_PUBLISHED_ISO": published_iso, + "STABLE_PUBLISHED_DATE": published_en, + "STABLE_PUBLISHED_DATE_ID": published_id, + "INSTALLER_SIZE": human_size(int(installer["sizeBytes"])), + "PORTABLE_SIZE": human_size(int(portable["sizeBytes"])), + "INSTALLER_SHA256": str(installer["sha256"]), + "PORTABLE_SHA256": str(portable["sha256"]), + "RELEASE_TITLE": str(notes["title"]), + "RELEASE_TITLE_ID": str(notes["titleId"]), + "RELEASE_SUMMARY": str(notes["summary"]), + "RELEASE_SUMMARY_ID": str(notes["summaryId"]), + "RELEASE_HIGHLIGHTS": list_html(require_list(notes["highlights"], "highlights")), + "RELEASE_HIGHLIGHTS_ID": list_html(require_list(notes["highlightsId"], "highlightsId")), + "RELEASE_IMPROVEMENTS": list_html(require_list(notes["improvements"], "improvements")), + "RELEASE_IMPROVEMENTS_ID": list_html(require_list(notes["improvementsId"], "improvementsId")), + "RELEASE_LIMITATIONS": list_html(require_list(notes["knownLimitations"], "knownLimitations")), + "RELEASE_LIMITATIONS_ID": list_html(require_list(notes["knownLimitationsId"], "knownLimitationsId")), + "SIGNING_STATUS": str(signing["status"]), + "SIGNING_LABEL": str(signing["label"]), + "SIGNING_LABEL_ID": str(signing["labelId"]), + "SIGNING_DETAIL": str(signing["detail"]), + "SIGNING_DETAIL_ID": str(signing["detailId"]), + "RELEASE_SCREENSHOT_SRC": str(screenshot["src"]), + "RELEASE_SCREENSHOT_WIDTH": str(screenshot["width"]), + "RELEASE_SCREENSHOT_HEIGHT": str(screenshot["height"]), + "RELEASE_SCREENSHOT_ALT": str(screenshot["alt"]), + "RELEASE_SCREENSHOT_ALT_ID": str(screenshot["altId"]), + "RELEASE_SCREENSHOT_CAPTION": str(screenshot["caption"]), + "RELEASE_SCREENSHOT_CAPTION_ID": str(screenshot["captionId"]), + "ISSUES_URL": str(notes["issuesUrl"]), + "RELEASE_URL": str(notes["releaseUrl"]), } @@ -225,7 +366,13 @@ def write_sitemap(output: Path, config: dict[str, object], pages: list[dict[str, (output / "sitemap.xml").write_text("\n".join(lines) + "\n", encoding="utf-8") -def write_build_info(output: Path, config: dict[str, object], version: str, pages: list[dict[str, object]]) -> None: +def write_build_info( + output: Path, + config: dict[str, object], + version: str, + stable_version: str, + pages: list[dict[str, object]], +) -> None: product = config["product"] index_now = config["indexNow"] assert isinstance(product, dict) and isinstance(index_now, dict) @@ -234,6 +381,7 @@ def write_build_info(output: Path, config: dict[str, object], version: str, page "schemaVersion": 3, "product": product["name"], "version": version, + "stableReleaseVersion": stable_version, "canonicalRoot": product["canonicalRoot"], "repository": product["repository"], "author": config["author"], @@ -248,10 +396,11 @@ def legacy_html_names() -> list[str]: return sorted(path.name for path in SOURCE.glob("*.html") if not VERIFICATION_PATTERN.fullmatch(path.name)) -def build(output: Path) -> None: +def build(output: Path, release_evidence_path: Path) -> None: version = read_version() config = read_config() - values = token_values(config, version) + evidence, notes = read_release_data(release_evidence_path) + values = token_values(config, version, evidence, notes) pages = page_registry(config) width, height = icon_dimensions() icon_size = f"{width}x{height}" @@ -265,6 +414,7 @@ def build(output: Path) -> None: shutil.copytree(SOURCE, output) shutil.rmtree(output / "templates", ignore_errors=True) shutil.rmtree(output / "partials", ignore_errors=True) + (output / "latest.json").write_text(json.dumps(evidence, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") generated: set[str] = set() for entry in pages: @@ -283,10 +433,10 @@ def build(output: Path) -> None: install_icon(output, icon_size) write_sitemap(output, config, pages) - write_build_info(output, config, version, pages) + write_build_info(output, config, version, str(evidence["version"]), pages) required = { - *generated, "site.json", "sitemap.xml", "build-info.json", str(index_now["keyFile"]), + *generated, "site.json", "latest.json", "release-notes.json", "sitemap.xml", "build-info.json", str(index_now["keyFile"]), "assets/app-icon.png", "assets/social-card.png", "assets/screenshots/arsas-first-launch.webp", "assets/screenshots/arsas-multi-ied.webp", "assets/screenshots/arsas-live-values.webp", "assets/screenshots/arsas-event-log.webp", @@ -305,14 +455,18 @@ def build(output: Path) -> None: ): if value in combined: raise SystemExit(f"Deployable product site still contains forbidden value: {value}") - print(f"Built ARSAS product website {version} at {output} ({len(generated)} pages, languages en/id, favicon {icon_size}).") + print( + f"Built ARSAS product website {version} with stable release {evidence['version']} at {output} " + f"({len(generated)} pages, languages en/id, favicon {icon_size})." + ) def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("--output", default=str(ROOT / "_site")) + parser.add_argument("--release-evidence", default=str(DEFAULT_RELEASE_EVIDENCE_PATH)) args = parser.parse_args() - build(Path(args.output).resolve()) + build(Path(args.output).resolve(), Path(args.release_evidence).resolve()) return 0 From f45c2c1a04df07d2202c9e0f3a3c9b2d5bf2c153 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:15:28 +0700 Subject: [PATCH 15/20] Build Pages from verified stable release evidence --- .github/workflows/pages.yml | 99 ++++++++++++++++--------------------- 1 file changed, 42 insertions(+), 57 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 599bb6cfb..3e2b28b6b 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -43,6 +43,40 @@ jobs: with: python-version: "3.12" + - name: Prepare verified stable release evidence + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: | + set -euo pipefail + if [ "$GITHUB_EVENT_NAME" = "pull_request" ]; then + cp landing/latest.json /tmp/arsas-published.json + else + gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > /tmp/arsas-published.json + fi + python - <<'PY' + import json + import re + from pathlib import Path + + evidence = json.loads(Path('/tmp/arsas-published.json').read_text()) + notes = json.loads(Path('landing/release-notes.json').read_text()) + if evidence.get('product') not in (None, 'ARSAS'): + raise SystemExit('Stable release evidence is for another product') + if evidence.get('channel') != 'stable': + raise SystemExit('Only stable release evidence may be deployed') + if evidence.get('version') != notes.get('version'): + raise SystemExit('Release notes and stable evidence versions differ') + if not re.fullmatch(r'\d+\.\d+\.\d+', str(evidence.get('version', ''))): + raise SystemExit('Stable release version is invalid') + for name in ('installer', 'portable'): + package = evidence.get(name, {}) + if not re.fullmatch(r'[0-9a-fA-F]{64}', str(package.get('sha256', ''))): + raise SystemExit(f'{name} SHA-256 is invalid') + if int(package.get('sizeBytes', 0)) < 1_000_000: + raise SystemExit(f'{name} size is invalid') + PY + - name: Validate product source, templates and SEO contract id: source_validation continue-on-error: true @@ -67,66 +101,11 @@ jobs: exit 1 - name: Build deterministic product website - run: python scripts/build-product-site.py --output _site + run: python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json - name: Validate IndexNow payload without network submission run: python scripts/submit-indexnow.py --sitemap _site/sitemap.xml --dry-run - - name: Sync verified stable updater manifest - if: github.event_name != 'pull_request' - env: - GH_TOKEN: ${{ github.token }} - shell: bash - run: | - set -euo pipefail - gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > /tmp/arsas-published.json - python - <<'PY' - import json - from pathlib import Path - - evidence = json.loads(Path('/tmp/arsas-published.json').read_text()) - installer = evidence['installer'] - manifest = { - 'schemaVersion': 1, - 'product': 'ARSAS', - 'version': evidence['version'], - 'channel': evidence['channel'], - 'publishedAtUtc': evidence['publishedAtUtc'], - 'installer': { - 'name': installer['name'], - 'url': installer['url'], - 'sha256': installer['sha256'], - 'sizeBytes': installer['sizeBytes'], - }, - } - Path('_site/latest.json').write_text(json.dumps(manifest, indent=2) + '\n') - PY - - - name: Add PR validation manifest - if: github.event_name == 'pull_request' - shell: bash - run: | - python - <<'PY' - import json - from pathlib import Path - - build = json.loads(Path('_site/build-info.json').read_text()) - manifest = { - 'schemaVersion': 1, - 'product': 'ARSAS', - 'version': build['version'], - 'channel': 'stable', - 'publishedAtUtc': '1970-01-01T00:00:00Z', - 'installer': { - 'name': 'ARSAS-Windows-x64-Setup.exe', - 'url': 'https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-Setup.exe', - 'sha256': '0' * 64, - 'sizeBytes': 1000000, - }, - } - Path('_site/latest.json').write_text(json.dumps(manifest, indent=2) + '\n') - PY - - name: Validate rendered product website id: build_validation continue-on-error: true @@ -195,8 +174,14 @@ jobs: with: python-version: "3.12" + - name: Prepare stable release evidence + env: + GH_TOKEN: ${{ github.token }} + shell: bash + run: gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > /tmp/arsas-published.json + - name: Build current sitemap - run: python scripts/build-product-site.py --output _site + run: python scripts/build-product-site.py --output _site --release-evidence /tmp/arsas-published.json - name: Submit deployed URLs through IndexNow id: indexnow From 3cfd669cd89acf418a76f69205dc9183b1af5a1e Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:17:32 +0700 Subject: [PATCH 16/20] Validate release trust source contracts --- scripts/validate-product-source.py | 113 ++++++++++++++++++++++++----- 1 file changed, 94 insertions(+), 19 deletions(-) diff --git a/scripts/validate-product-source.py b/scripts/validate-product-source.py index 3e86d7edd..063b5f592 100644 --- a/scripts/validate-product-source.py +++ b/scripts/validate-product-source.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate ARSAS product templates, authority, localization and source assets.""" +"""Validate ARSAS product templates, release trust, authority, localization and source assets.""" from __future__ import annotations @@ -18,10 +18,20 @@ INDEXNOW_SCRIPT = ROOT / "scripts" / "submit-indexnow.py" INCLUDE_PATTERN = re.compile(r"\{\{>\s*([a-z0-9-]+)\s*\}\}", re.IGNORECASE) VERIFICATION_PATTERN = re.compile(r"google[a-z0-9]+\.html", re.IGNORECASE) +SHA256_PATTERN = re.compile(r"[0-9a-fA-F]{64}") KNOWN_TOKENS = { "ARSAS_VERSION", "PRODUCT_NAME", "CANONICAL_ROOT", "REPOSITORY_URL", "ENGINE_REPOSITORY_URL", "AUTHOR_NAME", "AUTHOR_LINKEDIN", "AUTHOR_GITHUB", - "INSTALLER_URL", "PORTABLE_URL", "CHECKSUMS_URL", + "INSTALLER_URL", "PORTABLE_URL", "CHECKSUMS_URL", "STABLE_VERSION", + "STABLE_PUBLISHED_ISO", "STABLE_PUBLISHED_DATE", "STABLE_PUBLISHED_DATE_ID", + "INSTALLER_SIZE", "PORTABLE_SIZE", "INSTALLER_SHA256", "PORTABLE_SHA256", + "RELEASE_TITLE", "RELEASE_TITLE_ID", "RELEASE_SUMMARY", "RELEASE_SUMMARY_ID", + "RELEASE_HIGHLIGHTS", "RELEASE_HIGHLIGHTS_ID", "RELEASE_IMPROVEMENTS", + "RELEASE_IMPROVEMENTS_ID", "RELEASE_LIMITATIONS", "RELEASE_LIMITATIONS_ID", + "SIGNING_STATUS", "SIGNING_LABEL", "SIGNING_LABEL_ID", "SIGNING_DETAIL", + "SIGNING_DETAIL_ID", "RELEASE_SCREENSHOT_SRC", "RELEASE_SCREENSHOT_WIDTH", + "RELEASE_SCREENSHOT_HEIGHT", "RELEASE_SCREENSHOT_ALT", "RELEASE_SCREENSHOT_ALT_ID", + "RELEASE_SCREENSHOT_CAPTION", "RELEASE_SCREENSHOT_CAPTION_ID", "ISSUES_URL", "RELEASE_URL", } EXPECTED_NAV = ("overview", "capabilities", "solutions", "guides", "architecture", "about", "download") GUIDE_FILES = { @@ -33,6 +43,7 @@ LOCALIZED_PAIRS = { "": "id.html", "download.html": "unduh.html", + "release-notes.html": "catatan-rilis.html", "guides.html": "panduan.html", "mms-client.html": "mms-client-iec61850.html", "smart-reporting.html": "smart-reporting-iec61850.html", @@ -128,11 +139,11 @@ def validate_template(path: Path, content_type: str | None, language: str | None parser.feed(text) label = path.relative_to(ROOT) - if not parser.title.strip() or len(parser.title.strip()) > 75: + if not parser.title.strip() or len(parser.title.strip()) > 90: errors.append(f"{label}: invalid title") if parser.h1 != 1: errors.append(f"{label}: expected one h1, found {parser.h1}") - if not parser.description or not 70 <= len(parser.description) <= 220: + if not parser.description or not 70 <= len(parser.description) <= 240: errors.append(f"{label}: invalid meta description") if not parser.body_page: errors.append(f"{label}: missing body data-page") @@ -187,6 +198,10 @@ def validate_template(path: Path, content_type: str | None, language: str | None errors.append(f"{label}: localized page must declare Indonesian structured-data language") if 'hreflang="en"' not in raw: errors.append(f"{label}: localized page must link to an English source page") + if content_type == "release": + for value in ("What’s new", "Known limitations", "{{RELEASE_SCREENSHOT_SRC}}", "{{ISSUES_URL}}", "{{SIGNING_STATUS}}"): + if value not in raw: + errors.append(f"{label}: release page missing {value}") if content_type == "authority" and "Claim governance" not in raw: errors.append(f"{label}: authority page must document claim governance") @@ -238,7 +253,7 @@ def validate_registry(config: dict[str, object], errors: list[str]) -> list[tupl names: set[str] = set() entries: dict[str, dict[str, object]] = {} templates: list[tuple[Path, str | None, str | None]] = [] - guide_count = localized_count = authority_count = 0 + guide_count = localized_count = authority_count = release_count = 0 for entry in pages: if not isinstance(entry, dict): errors.append("landing/site.json: every page entry must be an object") @@ -275,15 +290,19 @@ def validate_registry(config: dict[str, object], errors: list[str]) -> list[tupl localized_count += 1 elif content_type == "authority": authority_count += 1 + elif content_type == "release": + release_count += 1 - if len(pages) != 44: - errors.append(f"landing/site.json: expected 44 pages, found {len(pages)}") + if len(pages) != 46: + errors.append(f"landing/site.json: expected 46 pages, found {len(pages)}") if guide_count != 11: errors.append(f"landing/site.json: expected 11 troubleshooting guides, found {guide_count}") - if localized_count != 12: - errors.append(f"landing/site.json: expected 12 Indonesian pages, found {localized_count}") + if localized_count != 13: + errors.append(f"landing/site.json: expected 13 Indonesian pages, found {localized_count}") if authority_count != 1: errors.append(f"landing/site.json: expected one authority page, found {authority_count}") + if release_count != 1: + errors.append(f"landing/site.json: expected one English release page, found {release_count}") for english, indonesian in LOCALIZED_PAIRS.items(): expected = {"en": english, "id": indonesian, "x-default": english} @@ -304,6 +323,51 @@ def validate_registry(config: dict[str, object], errors: list[str]) -> list[tupl return templates +def validate_release_sources(errors: list[str]) -> None: + try: + evidence = json.loads((LANDING / "latest.json").read_text(encoding="utf-8")) + notes = json.loads((LANDING / "release-notes.json").read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + errors.append(f"release trust JSON: {exc}") + return + version = str(evidence.get("version", "")) + if evidence.get("schemaVersion") != 1 or evidence.get("product") != "ARSAS" or evidence.get("channel") != "stable": + errors.append("landing/latest.json: invalid stable release identity") + if not re.fullmatch(r"\d+\.\d+\.\d+", version) or notes.get("version") != version: + errors.append("release evidence and release notes versions must match") + for key, expected_name in (("installer", "ARSAS-Windows-x64-Setup.exe"), ("portable", "ARSAS-Windows-x64-Portable.zip")): + package = evidence.get(key) + if not isinstance(package, dict) or package.get("name") != expected_name: + errors.append(f"landing/latest.json: invalid {key} identity") + continue + if not SHA256_PATTERN.fullmatch(str(package.get("sha256", ""))): + errors.append(f"landing/latest.json: invalid {key} SHA-256") + if not 1_000_000 <= int(package.get("sizeBytes", 0)) <= 500_000_000: + errors.append(f"landing/latest.json: invalid {key} size") + if not isinstance(evidence.get("checksums"), dict): + errors.append("landing/latest.json: checksum asset is missing") + signing = evidence.get("codeSigning") + note_signing = notes.get("codeSigning") + if not isinstance(signing, dict) or signing.get("status") not in {"signed", "unsigned"}: + errors.append("landing/latest.json: code-signing status is missing") + if not isinstance(note_signing, dict) or note_signing.get("status") != (signing or {}).get("status"): + errors.append("release notes signing status does not match evidence") + elif note_signing.get("status") == "unsigned" and "Authenticode" not in str(note_signing.get("detail", "")): + errors.append("unsigned release notes must explain Authenticode status") + for key in ("title", "titleId", "summary", "summaryId", "issuesUrl", "releaseUrl"): + if not isinstance(notes.get(key), str) or not str(notes.get(key)).strip(): + errors.append(f"landing/release-notes.json: missing {key}") + for key in ("highlights", "highlightsId", "improvements", "improvementsId", "knownLimitations", "knownLimitationsId"): + value = notes.get(key) + if not isinstance(value, list) or len(value) < 4: + errors.append(f"landing/release-notes.json: {key} must contain at least four entries") + screenshot = notes.get("screenshot") + if not isinstance(screenshot, dict) or not (LANDING / str(screenshot.get("src", ""))).exists(): + errors.append("landing/release-notes.json: current stable screenshot is missing") + if notes.get("issuesUrl") != "https://github.com/masarray/arsas/issues/new/choose": + errors.append("landing/release-notes.json: issue reporting URL is invalid") + + def validate_partials(errors: list[str]) -> None: def read(name: str) -> str: path = PARTIALS / name @@ -321,13 +385,13 @@ def read(name: str) -> str: for value in ('href="id.html"', 'hreflang="id"'): if value not in header + footer: errors.append(f"shared product chrome missing language route {value}") - for value in ("{{AUTHOR_NAME}}", "{{AUTHOR_LINKEDIN}}", "{{REPOSITORY_URL}}", 'href="solutions.html"', 'href="guides.html"', 'href="technical-review.html"'): + for value in ("{{AUTHOR_NAME}}", "{{AUTHOR_LINKEDIN}}", "{{REPOSITORY_URL}}", 'href="solutions.html"', 'href="guides.html"', 'href="technical-review.html"', 'href="release-notes.html"'): if value not in footer: errors.append(f"shared footer missing {value}") - for value in ("{{ARSAS_VERSION}}", "{{INSTALLER_URL}}", 'href="download.html"'): + for value in ("{{STABLE_VERSION}}", "{{INSTALLER_URL}}", 'href="download.html"', 'href="release-notes.html"'): if value not in cta: errors.append(f"shared download CTA missing {value}") - for value in ("{{ARSAS_VERSION}}", "{{INSTALLER_URL}}", 'href="unduh.html"', "Unduh installer"): + for value in ("{{STABLE_VERSION}}", "{{INSTALLER_URL}}", 'href="unduh.html"', 'href="catatan-rilis.html"', "Unduh Windows installer"): if value not in cta_id: errors.append(f"shared Indonesian download CTA missing {value}") for value in ("Ari Sulistiono", 'href="guides.html"', 'href="technical-review.html"', "Engineering boundary"): @@ -344,10 +408,11 @@ def template(name: str) -> str: solutions, guides = template("solutions.html"), template("guides.html") review, localized_home = template("technical-review.html"), template("id.html") localized_guides, localized_download = template("panduan.html"), template("unduh.html") + release, release_id = template("release-notes.html"), template("catatan-rilis.html") for value in ("{{INSTALLER_URL}}", 'href="download.html"', 'href="solutions.html"', "arsas-rcb-scl-export.webp", "{{AUTHOR_LINKEDIN}}", '"codeRepository"'): if value not in home: errors.append(f"homepage template missing {value}") - for value in ("{{INSTALLER_URL}}", "{{PORTABLE_URL}}", "{{CHECKSUMS_URL}}", "{{ARSAS_VERSION}}", "Download Center"): + for value in ("{{INSTALLER_URL}}", "{{PORTABLE_URL}}", "{{CHECKSUMS_URL}}", "{{STABLE_VERSION}}", "Installer vs Portable", "data-copy-value", "{{SIGNING_STATUS}}", 'href="release-notes.html"', "{{ISSUES_URL}}"): if value not in download: errors.append(f"download template missing {value}") for value in ("{{AUTHOR_LINKEDIN}}", "{{AUTHOR_GITHUB}}", "{{REPOSITORY_URL}}", "Download Center"): @@ -365,15 +430,21 @@ def template(name: str) -> str: for value in ("Pengujian IEC 61850", 'href="panduan.html"', 'href="unduh.html"', 'hreflang="en"'): if value not in localized_home: errors.append(f"Indonesian homepage missing {value}") - for localized in LOCALIZED_FILES - {"id.html", "panduan.html", "unduh.html"}: + for localized in LOCALIZED_FILES - {"id.html", "panduan.html", "unduh.html", "catatan-rilis.html"}: if f'href="{localized}"' not in localized_home and f'href="{localized}"' not in localized_guides: errors.append(f"Indonesian hubs do not link to {localized}") for value in ("Panduan Troubleshooting", 'href="reporting-silent.html"', 'href="technical-review.html"', 'href="mms-client-iec61850.html"'): if value not in localized_guides: errors.append(f"Indonesian guide hub missing {value}") - for value in ("{{INSTALLER_URL}}", "{{PORTABLE_URL}}", "{{CHECKSUMS_URL}}", "Unduh ARSAS", "{{> download-cta-id}}"): + for value in ("{{INSTALLER_URL}}", "{{PORTABLE_URL}}", "{{CHECKSUMS_URL}}", "{{STABLE_VERSION}}", "Installer vs Portable", "data-copy-value", "{{SIGNING_STATUS}}", 'href="catatan-rilis.html"', "{{ISSUES_URL}}", "{{> download-cta-id}}"): if value not in localized_download: errors.append(f"Indonesian download page missing {value}") + for value in ("What’s new", "Known limitations", "{{RELEASE_HIGHLIGHTS}}", "{{RELEASE_SCREENSHOT_SRC}}", "{{SIGNING_DETAIL}}", "{{ISSUES_URL}}", "{{> download-cta}}"): + if value not in release: + errors.append(f"English release notes missing {value}") + for value in ("Yang baru", "Known limitations", "{{RELEASE_HIGHLIGHTS_ID}}", "{{RELEASE_SCREENSHOT_SRC}}", "{{SIGNING_DETAIL_ID}}", "{{ISSUES_URL}}", 'hreflang="en"', "{{> download-cta-id}}"): + if value not in release_id: + errors.append(f"Indonesian release notes missing {value}") localized_contracts = { "mms-client-iec61850.html": ("MMS Client", "DataSet", "mms-client.html"), @@ -400,9 +471,9 @@ def template(name: str) -> str: if (LANDING / "sitemap.xml").exists(): errors.append("landing/sitemap.xml must be generated from site.json, not stored as a second source") for relative in ( - "assets/screenshots/arsas-first-launch.webp", "assets/screenshots/arsas-rcb-scl-export.webp", - "assets/social-card.png", "site.webmanifest", "robots.txt", - "arsas-iec61850-20260720-6f4a9d2c8b.txt", + "assets/screenshots/arsas-first-launch.webp", "assets/screenshots/arsas-live-values.webp", + "assets/screenshots/arsas-rcb-scl-export.webp", "assets/social-card.png", "site.webmanifest", + "robots.txt", "latest.json", "release-notes.json", "arsas-iec61850-20260720-6f4a9d2c8b.txt", ): if not (LANDING / relative).exists(): errors.append(f"missing landing source file: {relative}") @@ -423,6 +494,7 @@ def main() -> int: errors: list[str] = [] config = read_config(errors) templates = validate_registry(config, errors) if config else [] + validate_release_sources(errors) validate_partials(errors) for template_path, content_type, language in templates: validate_template(template_path, content_type, language, errors) @@ -434,7 +506,10 @@ def main() -> int: print(f"- {error}", file=sys.stderr) return 1 width, height = png_size(APP_ICON_SOURCE) - print(f"ARSAS product-source validation passed: {len(templates)} templates, 11 guides, 12 Indonesian pages and latest {width}x{height} app icon.") + print( + f"ARSAS product-source validation passed: {len(templates)} templates, 11 guides, " + f"13 Indonesian pages, 13 hreflang pairs, stable release trust and latest {width}x{height} app icon." + ) return 0 From 2c2f17add5e3b059b71c9d7fc020452cf2ad8bac Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:19:19 +0700 Subject: [PATCH 17/20] Validate rendered release trust experience --- scripts/validate-product-build.py | 154 +++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 44 deletions(-) diff --git a/scripts/validate-product-build.py b/scripts/validate-product-build.py index ce521ee1a..7fd8d7443 100644 --- a/scripts/validate-product-build.py +++ b/scripts/validate-product-build.py @@ -1,5 +1,5 @@ #!/usr/bin/env python3 -"""Validate the rendered ARSAS product website.""" +"""Validate the rendered ARSAS product website and stable release trust contracts.""" from __future__ import annotations @@ -22,6 +22,7 @@ INSTALLER = "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-Setup.exe" PORTABLE = "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-Portable.zip" CHECKSUMS = "https://github.com/masarray/arsas/releases/latest/download/ARSAS-Windows-x64-SHA256SUMS.txt" +ISSUES = "https://github.com/masarray/arsas/issues/new/choose" REPOSITORY = "https://github.com/masarray/arsas" LINKEDIN = "https://www.linkedin.com/in/ari-sulistiono" AUTHOR_GITHUB = "https://github.com/masarray" @@ -39,6 +40,7 @@ LOCALIZED_PAIRS = { "index.html": "id.html", "download.html": "unduh.html", + "release-notes.html": "catatan-rilis.html", "guides.html": "panduan.html", "mms-client.html": "mms-client-iec61850.html", "smart-reporting.html": "smart-reporting-iec61850.html", @@ -127,41 +129,94 @@ def page_url(name: str) -> str: return CANONICAL_ROOT if name == "index.html" else CANONICAL_ROOT + name -def validate_latest(site: Path, errors: list[str]) -> None: +def validate_latest(site: Path, errors: list[str]) -> tuple[str | None, dict[str, object] | None]: path = site / "latest.json" if not path.exists(): errors.append("missing latest.json") - return + return None, None try: manifest = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: errors.append(f"latest.json: {exc}") - return + return None, None + version = str(manifest.get("version", "")) if manifest.get("schemaVersion") != 1 or manifest.get("product") != "ARSAS": errors.append("latest.json has invalid identity") if manifest.get("channel") != "stable": errors.append("latest.json channel must be stable") - if not re.fullmatch(r"\d+\.\d+\.\d+", str(manifest.get("version", ""))): + if not re.fullmatch(r"\d+\.\d+\.\d+", version): errors.append("latest.json version is invalid") - installer = manifest.get("installer") - if not isinstance(installer, dict) or installer.get("url") != INSTALLER: - errors.append("latest.json installer URL is invalid") - elif not re.fullmatch(r"[0-9a-fA-F]{64}", str(installer.get("sha256", ""))): - errors.append("latest.json installer SHA-256 is invalid") - - -def validate_build_info(site: Path, errors: list[str]) -> tuple[str | None, list[str]]: + expected = { + "installer": ("ARSAS-Windows-x64-Setup.exe", INSTALLER), + "portable": ("ARSAS-Windows-x64-Portable.zip", PORTABLE), + } + for key, (name, url) in expected.items(): + package = manifest.get(key) + if not isinstance(package, dict) or package.get("name") != name or package.get("url") != url: + errors.append(f"latest.json {key} identity is invalid") + continue + if not re.fullmatch(r"[0-9a-fA-F]{64}", str(package.get("sha256", ""))): + errors.append(f"latest.json {key} SHA-256 is invalid") + if not 1_000_000 <= int(package.get("sizeBytes", 0)) <= 500_000_000: + errors.append(f"latest.json {key} size is invalid") + checksums = manifest.get("checksums") + if not isinstance(checksums, dict) or checksums.get("url") != CHECKSUMS: + errors.append("latest.json checksums URL is invalid") + if not str(manifest.get("publishedAtUtc", "")): + errors.append("latest.json publishedAtUtc is missing") + signing = manifest.get("codeSigning") + if not isinstance(signing, dict) or signing.get("status") not in {"signed", "unsigned"}: + errors.append("latest.json code-signing status is invalid") + return version or None, manifest + + +def validate_release_notes(site: Path, stable_version: str | None, evidence: dict[str, object] | None, errors: list[str]) -> None: + path = site / "release-notes.json" + if not path.exists(): + errors.append("missing release-notes.json") + return + try: + notes = json.loads(path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError) as exc: + errors.append(f"release-notes.json: {exc}") + return + if notes.get("schemaVersion") != 1 or notes.get("product") != "ARSAS" or notes.get("version") != stable_version: + errors.append("release-notes.json identity or version is invalid") + for key in ("title", "titleId", "summary", "summaryId", "issuesUrl", "releaseUrl"): + if not isinstance(notes.get(key), str) or not str(notes.get(key)).strip(): + errors.append(f"release-notes.json missing {key}") + for key in ("highlights", "highlightsId", "improvements", "improvementsId", "knownLimitations", "knownLimitationsId"): + value = notes.get(key) + if not isinstance(value, list) or len(value) < 4: + errors.append(f"release-notes.json {key} is incomplete") + if notes.get("issuesUrl") != ISSUES: + errors.append("release-notes.json issue reporting URL is invalid") + signing = notes.get("codeSigning") + evidence_signing = evidence.get("codeSigning") if isinstance(evidence, dict) else None + if not isinstance(signing, dict) or not isinstance(evidence_signing, dict) or signing.get("status") != evidence_signing.get("status"): + errors.append("release-notes.json signing status does not match latest.json") + elif signing.get("status") == "unsigned" and "Authenticode" not in str(signing.get("detail", "")): + errors.append("unsigned release notes do not explain Authenticode status") + screenshot = notes.get("screenshot") + if not isinstance(screenshot, dict) or not (site / str(screenshot.get("src", ""))).exists(): + errors.append("release-notes.json stable screenshot is missing") + + +def validate_build_info(site: Path, errors: list[str]) -> tuple[str | None, str | None, list[str]]: path = site / "build-info.json" try: info = json.loads(path.read_text(encoding="utf-8")) except (OSError, json.JSONDecodeError) as exc: errors.append(f"build-info.json: {exc}") - return None, [] + return None, None, [] if info.get("schemaVersion") != 3: errors.append("build-info.json schemaVersion must be 3") version = str(info.get("version", "")) + stable_version = str(info.get("stableReleaseVersion", "")) if not re.fullmatch(r"\d+\.\d+\.\d+", version): errors.append("build-info.json version is invalid") + if not re.fullmatch(r"\d+\.\d+\.\d+", stable_version): + errors.append("build-info.json stableReleaseVersion is invalid") if info.get("repository") != REPOSITORY: errors.append("build-info.json repository is invalid") author = info.get("author") @@ -174,16 +229,16 @@ def validate_build_info(site: Path, errors: list[str]) -> tuple[str | None, list pages = info.get("pages") if not isinstance(pages, list) or not pages or not all(isinstance(page, str) for page in pages): errors.append("build-info.json pages registry is invalid") - return version or None, [] + return version or None, stable_version or None, [] if len(pages) != len(set(pages)): errors.append("build-info.json pages registry contains duplicates") - if len(pages) != 44: - errors.append(f"build-info.json must contain 44 pages, found {len(pages)}") + if len(pages) != 46: + errors.append(f"build-info.json must contain 46 pages, found {len(pages)}") if not GUIDE_PAGES.issubset(set(pages)): errors.append("build-info.json is missing troubleshooting guide pages") - if not LOCALIZED_PAGES.issubset(set(pages)) or "technical-review.html" not in pages: - errors.append("build-info.json is missing authority or Indonesian pages") - return version or None, list(pages) + if not LOCALIZED_PAGES.issubset(set(pages)) or "technical-review.html" not in pages or "release-notes.html" not in pages: + errors.append("build-info.json is missing authority, release or Indonesian pages") + return version or None, stable_version or None, list(pages) def validate_sitemap(site: Path, pages: list[str], errors: list[str]) -> None: @@ -223,15 +278,11 @@ def validate_sitemap(site: Path, pages: list[str], errors: list[str]) -> None: errors.append(f"sitemap.xml missing {missing}") for extra in sorted(set(records) - expected_urls): errors.append(f"sitemap.xml contains unexpected URL {extra}") - if len(records) != 43: - errors.append(f"sitemap.xml must contain 43 indexable URLs, found {len(records)}") + if len(records) != 45: + errors.append(f"sitemap.xml must contain 45 indexable URLs, found {len(records)}") for english, indonesian in LOCALIZED_PAIRS.items(): - expected = { - "en": page_url(english), - "id": page_url(indonesian), - "x-default": page_url(english), - } + expected = {"en": page_url(english), "id": page_url(indonesian), "x-default": page_url(english)} for page in (english, indonesian): if records.get(page_url(page)) != expected: errors.append(f"sitemap.xml localized alternate set is incomplete for {page}") @@ -258,19 +309,21 @@ def validate_manifest(site: Path, icon_size: str, errors: list[str]) -> None: def expected_alternates_for(name: str) -> dict[str, str] | None: for english, indonesian in LOCALIZED_PAIRS.items(): if name in (english, indonesian): - return { - "en": page_url(english), - "id": page_url(indonesian), - "x-default": page_url(english), - } + return {"en": page_url(english), "id": page_url(indonesian), "x-default": page_url(english)} return None def main() -> int: site = Path(sys.argv[1] if len(sys.argv) > 1 else "_site").resolve() errors: list[str] = [] - version, pages = validate_build_info(site, errors) - for relative in tuple(pages) + EXPECTED_MEDIA + ("site.json", "build-info.json", "sitemap.xml", "site.webmanifest", INDEXNOW_FILE): + version, stable_version, pages = validate_build_info(site, errors) + stable_from_latest, evidence = validate_latest(site, errors) + if stable_version and stable_from_latest and stable_version != stable_from_latest: + errors.append("build-info.json stable release does not match latest.json") + validate_release_notes(site, stable_from_latest, evidence, errors) + for relative in tuple(pages) + EXPECTED_MEDIA + ( + "site.json", "latest.json", "release-notes.json", "build-info.json", "sitemap.xml", "site.webmanifest", INDEXNOW_FILE, + ): if not (site / relative).exists(): errors.append(f"missing deployable file: {relative}") key_path = site / INDEXNOW_FILE @@ -331,9 +384,9 @@ def main() -> int: for image in brand_images: if image.get("width") != str(icon_width) or image.get("height") != str(icon_height): errors.append(f"{name}: brand mark metadata must match {icon_size}") - for value in (LINKEDIN, REPOSITORY, 'href="download.html"', 'href="technical-review.html"'): + for value in (LINKEDIN, REPOSITORY, 'href="download.html"', 'href="technical-review.html"', 'href="release-notes.html"'): if value not in text: - errors.append(f"{name}: shared authority or download route missing {value}") + errors.append(f"{name}: shared authority, release or download route missing {value}") expected_alternates = expected_alternates_for(name) if expected_alternates is not None and parser.alternates != expected_alternates: errors.append(f"{name}: page-level hreflang alternates are incomplete") @@ -352,7 +405,7 @@ def main() -> int: errors.append(f"{name}: Indonesian language metadata is incomplete") if 'hreflang="en"' not in text: errors.append(f"{name}: missing English counterpart link") - for value in ('href="unduh.html"', "Semua opsi unduhan"): + for value in ('href="unduh.html"', "Verifikasi dan bandingkan paket"): if value not in text: errors.append(f"{name}: Indonesian download CTA is incomplete: {value}") @@ -373,12 +426,13 @@ def page_text(name: str) -> str: solutions, guides = page_text("solutions.html"), page_text("guides.html") review, id_home = page_text("technical-review.html"), page_text("id.html") id_guides, id_download = page_text("panduan.html"), page_text("unduh.html") + release, release_id = page_text("release-notes.html"), page_text("catatan-rilis.html") for value in (INSTALLER, 'href="download.html"', 'href="solutions.html"', "arsas-rcb-scl-export.webp", '"codeRepository"'): if value not in home: errors.append(f"homepage missing product contract: {value}") - for value in (INSTALLER, PORTABLE, CHECKSUMS, "Latest stable channel", "Download Center"): + for value in (INSTALLER, PORTABLE, CHECKSUMS, "Installer vs Portable", "Copy installer SHA-256", "Known limitations", "Not Authenticode-signed", ISSUES, 'href="release-notes.html"'): if value not in download: - errors.append(f"download page missing {value}") + errors.append(f"download page missing release trust value {value}") for value in (LINKEDIN, AUTHOR_GITHUB, REPOSITORY, "Download Center"): if value not in about + home: errors.append(f"author or open-source identity missing {value}") @@ -391,12 +445,18 @@ def page_text(name: str) -> str: for value in ("Claim governance", "Not a conformance certificate", LINKEDIN, REPOSITORY): if value not in review: errors.append(f"technical review page missing {value}") - for localized in LOCALIZED_PAGES - {"id.html", "panduan.html", "unduh.html"}: + for localized in LOCALIZED_PAGES - {"id.html", "panduan.html", "unduh.html", "catatan-rilis.html"}: if f'href="{localized}"' not in id_home and f'href="{localized}"' not in id_guides: errors.append(f"Indonesian hubs do not link to {localized}") - for value in (INSTALLER, PORTABLE, CHECKSUMS, "Unduh ARSAS", "Semua opsi unduhan"): + for value in (INSTALLER, PORTABLE, CHECKSUMS, "Installer vs Portable", "Salin SHA-256 installer", "Known limitations", "Belum ditandatangani dengan Authenticode", ISSUES, 'href="catatan-rilis.html"', "Verifikasi dan bandingkan paket"): if value not in id_download: - errors.append(f"Indonesian download page missing {value}") + errors.append(f"Indonesian download page missing release trust value {value}") + for value in ("What’s new", "Known limitations", "Not Authenticode-signed", ISSUES, "arsas-live-values.webp", "Download Windows installer"): + if value not in release: + errors.append(f"English release notes missing {value}") + for value in ("Yang baru", "Known limitations", "Belum ditandatangani dengan Authenticode", ISSUES, "arsas-live-values.webp", "Unduh Windows installer"): + if value not in release_id: + errors.append(f"Indonesian release notes missing {value}") localized_contracts = { "mms-client-iec61850.html": ("MMS Client", "DataSet"), @@ -417,9 +477,12 @@ def page_text(name: str) -> str: if version and f'"softwareVersion":"{version}"' not in home.replace(" ", ""): errors.append("homepage softwareVersion does not match build-info.json") + if stable_version: + for name, text in (("download.html", download), ("unduh.html", id_download), ("release-notes.html", release), ("catatan-rilis.html", release_id)): + if stable_version not in text: + errors.append(f"{name}: stable release version is missing") validate_sitemap(site, pages, errors) - validate_latest(site, errors) if icon_size: validate_manifest(site, icon_size, errors) social = site / "assets/social-card.png" @@ -436,7 +499,10 @@ def page_text(name: str) -> str: for error in errors: print(f"- {error}", file=sys.stderr) return 1 - print(f"ARSAS product-build validation passed: {len(pages)} pages, 11 guides, 12 Indonesian pages, 12 hreflang pairs, authority policy, IndexNow and {icon_size} brand artwork.") + print( + f"ARSAS product-build validation passed: {len(pages)} pages, 11 guides, 13 Indonesian pages, " + f"13 hreflang pairs, 45 indexable URLs, release trust, authority policy, IndexNow and {icon_size} brand artwork." + ) return 0 From 7d089edf8375b8d7e95787827bd6dd8d608b8e4c Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:20:46 +0700 Subject: [PATCH 18/20] Publish release notes from trusted metadata --- .../workflows/publish-verified-release.yml | 84 +++++++++++++++---- 1 file changed, 69 insertions(+), 15 deletions(-) diff --git a/.github/workflows/publish-verified-release.yml b/.github/workflows/publish-verified-release.yml index 3fdc3a8d7..0e07df8d4 100644 --- a/.github/workflows/publish-verified-release.yml +++ b/.github/workflows/publish-verified-release.yml @@ -5,11 +5,13 @@ on: branches: [ main ] paths: - ".release/publish-verified.json" + - "landing/release-notes.json" - ".github/workflows/publish-verified-release.yml" push: branches: [ main ] paths: - ".release/publish-verified.json" + - "landing/release-notes.json" - ".github/workflows/publish-verified-release.yml" workflow_dispatch: @@ -26,16 +28,18 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Read verified publication request + - name: Read verified publication request and release notes id: request shell: bash run: | set -euo pipefail python - <<'PY' >> "$GITHUB_OUTPUT" import json + import re from pathlib import Path request = json.loads(Path('.release/publish-verified.json').read_text()) + notes = json.loads(Path('landing/release-notes.json').read_text()) required = ( 'version', 'installerArtifactId', 'installerSha256', 'portableArtifactId', 'portableSha256' @@ -43,6 +47,13 @@ jobs: missing = [key for key in required if not request.get(key)] if missing: raise SystemExit('Missing publication request fields: ' + ', '.join(missing)) + if not re.fullmatch(r'\d+\.\d+\.\d+', str(request['version'])): + raise SystemExit('Publication request version is invalid') + if notes.get('version') != request['version']: + raise SystemExit('Release notes version does not match publication request') + signing = notes.get('codeSigning', {}) + if signing.get('status') not in ('signed', 'unsigned'): + raise SystemExit('Release notes must declare code-signing status') print(f"version={request['version']}") print(f"tag=v{request['version']}") @@ -50,6 +61,7 @@ jobs: print(f"installer_sha256={request['installerSha256'].lower()}") print(f"portable_artifact_id={request['portableArtifactId']}") print(f"portable_sha256={request['portableSha256'].lower()}") + print(f"signing_status={signing['status']}") PY - name: Download tested workflow artifacts @@ -92,6 +104,51 @@ jobs: sha256sum verified/ARSAS-Windows-x64-Portable.zip | sed 's#verified/##' } > verified/ARSAS-Windows-x64-SHA256SUMS.txt + - name: Build public release notes from reviewed metadata + env: + VERSION: ${{ steps.request.outputs.version }} + shell: bash + run: | + python - <<'PY' + import json + import os + from pathlib import Path + + notes = json.loads(Path('landing/release-notes.json').read_text()) + version = os.environ['VERSION'] + lines = [ + f"# ARSAS {version}", + "", + notes['summary'], + "", + "## What's new", + "", + *[f"- {item}" for item in notes['highlights']], + "", + "## Reliability improvements", + "", + *[f"- {item}" for item in notes['improvements']], + "", + "## Known limitations", + "", + *[f"- {item}" for item in notes['knownLimitations']], + "", + "## Code-signing status", + "", + f"**{notes['codeSigning']['label']}.** {notes['codeSigning']['detail']}", + "", + "## Included assets", + "", + "- Windows x64 installer", + "- Portable Windows x64 ZIP", + "- SHA-256 checksums", + "", + "Verify the SHA-256 value before use and report reproducible problems through the project issue tracker.", + "", + ] + Path('verified/release-notes.md').write_text('\n'.join(lines), encoding='utf-8') + PY + - name: Publish or update stable GitHub release if: github.event_name != 'pull_request' env: @@ -101,19 +158,6 @@ jobs: shell: bash run: | set -euo pipefail - cat > verified/release-notes.md < verified/published.json import json from datetime import datetime, timezone + from pathlib import Path + notes = json.loads(Path('landing/release-notes.json').read_text()) print(json.dumps({ + 'schemaVersion': 1, + 'product': 'ARSAS', 'version': '$VERSION', 'tag': '$TAG', 'channel': 'stable', 'publishedAtUtc': datetime.now(timezone.utc).isoformat(), 'sourceCommit': '$GITHUB_SHA', + 'releaseUrl': 'https://github.com/$GITHUB_REPOSITORY/releases/tag/$TAG', 'installer': { 'name': 'ARSAS-Windows-x64-Setup.exe', 'url': 'https://github.com/$GITHUB_REPOSITORY/releases/latest/download/ARSAS-Windows-x64-Setup.exe', @@ -199,6 +248,11 @@ jobs: 'name': 'ARSAS-Windows-x64-SHA256SUMS.txt', 'url': 'https://github.com/$GITHUB_REPOSITORY/releases/latest/download/ARSAS-Windows-x64-SHA256SUMS.txt', }, + 'codeSigning': { + 'status': notes['codeSigning']['status'], + 'platform': 'Authenticode', + 'detail': notes['codeSigning']['detail'], + }, }, indent=2)) PY @@ -215,7 +269,7 @@ jobs: if [ -n "$existing_sha" ]; then args+=(-f sha="$existing_sha"); fi gh api --method PUT "$api_path" "${args[@]}" >/dev/null - - name: Refresh product website updater manifest + - name: Refresh product website release metadata if: github.event_name != 'pull_request' env: GH_TOKEN: ${{ github.token }} From 2f302652dcbf0811649306a945feaab996b46f40 Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:23:31 +0700 Subject: [PATCH 19/20] Restore visible English download route --- landing/templates/unduh.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/landing/templates/unduh.html b/landing/templates/unduh.html index 15891d545..026372f2f 100644 --- a/landing/templates/unduh.html +++ b/landing/templates/unduh.html @@ -33,7 +33,7 @@ {{> header}}
    -
    Pusat Unduhan Resmi · stable release · Windows x64

    Unduh ARSAS {{STABLE_VERSION}} dengan evidence release yang dapat diverifikasi.

    Pilih installer untuk workstation engineering normal atau portable ZIP untuk deployment berbasis folder yang terkontrol. Kedua paket berasal dari artifact CI yang diverifikasi dan memiliki SHA-256 publik.

    Stable terbaru · ARSAS {{STABLE_VERSION}} · dipublikasikan
    +
    Pusat Unduhan Resmi · stable release · Windows x64

    Unduh ARSAS {{STABLE_VERSION}} dengan evidence release yang dapat diverifikasi.

    Pilih installer untuk workstation engineering normal atau portable ZIP untuk deployment berbasis folder yang terkontrol. Kedua paket berasal dari artifact CI yang diverifikasi dan memiliki SHA-256 publik.

    Stable terbaru · ARSAS {{STABLE_VERSION}} · dipublikasikan
    Berbasis folder

    Portable ZIP

    Cocok untuk laptop pengujian, evaluasi sementara atau lingkungan yang tidak menginginkan instalasi. Ekstrak ke folder lokal yang writable dan update dengan mengganti paket.

    File
    ARSAS-Windows-x64-Portable.zip
    Ukuran
    {{PORTABLE_SIZE}}
    Publish
    {{STABLE_PUBLISHED_DATE_ID}}
    Signing
    {{SIGNING_LABEL_ID}}
    Unduh portable ZIP
    From 86d54ca2e5292074435e6baa42853f9bd361784d Mon Sep 17 00:00:00 2001 From: masarray Date: Mon, 20 Jul 2026 16:28:03 +0700 Subject: [PATCH 20/20] Preserve original stable release publication identity --- .github/workflows/publish-verified-release.yml | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/.github/workflows/publish-verified-release.yml b/.github/workflows/publish-verified-release.yml index 0e07df8d4..5593cfe4a 100644 --- a/.github/workflows/publish-verified-release.yml +++ b/.github/workflows/publish-verified-release.yml @@ -11,8 +11,6 @@ on: branches: [ main ] paths: - ".release/publish-verified.json" - - "landing/release-notes.json" - - ".github/workflows/publish-verified-release.yml" workflow_dispatch: permissions: @@ -216,6 +214,9 @@ jobs: portable_sha="$(sha256sum verified/ARSAS-Windows-x64-Portable.zip | awk '{print $1}')" installer_size="$(stat -c %s verified/ARSAS-Windows-x64-Setup.exe)" portable_size="$(stat -c %s verified/ARSAS-Windows-x64-Portable.zip)" + if ! gh api "repos/$GITHUB_REPOSITORY/contents/published.json?ref=release-evidence" --jq .content | base64 -d > verified/existing-published.json; then + printf '{}\n' > verified/existing-published.json + fi python - < verified/published.json import json @@ -223,14 +224,21 @@ jobs: from pathlib import Path notes = json.loads(Path('landing/release-notes.json').read_text()) + try: + existing = json.loads(Path('verified/existing-published.json').read_text()) + except (json.JSONDecodeError, OSError): + existing = {} + same_version = existing.get('version') == '$VERSION' + published_at = existing.get('publishedAtUtc') if same_version else datetime.now(timezone.utc).isoformat() + source_commit = existing.get('sourceCommit') if same_version else '$GITHUB_SHA' print(json.dumps({ 'schemaVersion': 1, 'product': 'ARSAS', 'version': '$VERSION', 'tag': '$TAG', 'channel': 'stable', - 'publishedAtUtc': datetime.now(timezone.utc).isoformat(), - 'sourceCommit': '$GITHUB_SHA', + 'publishedAtUtc': published_at, + 'sourceCommit': source_commit, 'releaseUrl': 'https://github.com/$GITHUB_REPOSITORY/releases/tag/$TAG', 'installer': { 'name': 'ARSAS-Windows-x64-Setup.exe',