Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 95 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: release

on:
push:
tags:
- "v*"

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
# The release body is the tag annotation, which release-tools:new writes; this job is the only
# publisher of the release.
build-and-release:
name: Build and release
runs-on: windows-latest
permissions:
contents: write

steps:
- name: checkout
uses: actions/checkout@v7
with:
persist-credentials: false
fetch-depth: 0

- name: extract version and release notes from tag
id: version
shell: bash
run: |
TAG="${GITHUB_REF_NAME}"
VERSION="${TAG#v}"
# Inno Setup's VersionInfoVersion accepts digits and dots only.
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "tag $TAG is not vMAJOR.MINOR.PATCH" >&2
exit 1
fi
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
echo "tag=$TAG" >> "$GITHUB_OUTPUT"

BODY=$(git tag -l --format='%(contents)' "$TAG")
if [ -z "$BODY" ]; then
BODY="Release $VERSION"
echo "no tag annotation found, using default body"
fi

{
echo "body<<RELEASE_NOTES_EOF"
echo "$BODY"
echo "RELEASE_NOTES_EOF"
} >> "$GITHUB_OUTPUT"

- name: setup .NET
uses: actions/setup-dotnet@v6
with:
global-json-file: global.json

- name: restore
run: dotnet restore SemiPlot.slnx

- name: build
run: dotnet build SemiPlot.slnx -c Release --no-restore

- name: unit tests
run: dotnet test SemiPlot/SemiPlot.Tests.Unit/SemiPlot.Tests.Unit.csproj -c Release --no-build

- name: publish
run: dotnet publish SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj -c Release -p:Version=${{ steps.version.outputs.version }}

# The jrsoftware.org direct download intermittently serves a corrupted file, so winget
# (preinstalled on windows runners) installs it instead. Machine scope keeps the
# Program Files (x86) path the build-installer step expects.
- name: install Inno Setup
shell: powershell
run: |
winget install --id JRSoftware.InnoSetup --exact --scope machine --silent `
--accept-source-agreements --accept-package-agreements --disable-interactivity
echo "C:\Program Files (x86)\Inno Setup 6" | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append

- name: build installer
shell: cmd
run: iscc.exe /DAppVersion=${{ steps.version.outputs.version }} Installer/SemiPlot.iss

- name: create GitHub release
uses: softprops/action-gh-release@v3
with:
tag_name: ${{ steps.version.outputs.tag }}
name: SemiPlot ${{ steps.version.outputs.version }}
body: ${{ steps.version.outputs.body }}
draft: false
prerelease: false
make_latest: true
files: |
Installer/Output/SemiPlot-Setup.exe
79 changes: 79 additions & 0 deletions Installer/SemiPlot.iss
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
; SemiPlot Inno Setup installer script
; Build with: iscc.exe /DAppVersion=1.2.3 Installer\SemiPlot.iss
; Run `dotnet publish SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj -c Release -p:Version=1.2.3` first.

#ifndef AppVersion
#error AppVersion must be defined. Pass it as: iscc.exe /DAppVersion=1.2.3 SemiPlot.iss
#endif

#define AppName "SemiPlot"
#define AppPublisher "Inc Semiteq"
#define AppExeName "SemiPlot.UI.exe"
#define AppId "{{D1A2B63D-5F0B-4A3E-9437-885417FCE2BC}"

; Paths relative to the location of this .iss file (Installer/)
#define SrcBinDir "..\SemiPlot\Artifacts\publish\SemiPlot.UI\release_win-x64"
#define SrcCfgDir "..\ConfigFiles"
#define AppIconFile "..\SemiPlot\SemiPlot.UI\logo.ico"

; Site paths: docs/architecture/overview.md#deployment
#define ConfigRoot "C:\DISTR\Config\SemiPlot"
#define LogRoot "C:\DISTR\Logs\SemiPlot"
#define LaunchArgs '--config-dir ""' + ConfigRoot + '"" --log-file ""' + LogRoot + '\semiplot.log"" --logging-level information'

[Setup]
AppId={#AppId}
AppName={#AppName}
AppVersion={#AppVersion}
AppPublisher={#AppPublisher}
AppVerName={#AppName} {#AppVersion}
VersionInfoVersion={#AppVersion}

ArchitecturesAllowed=x64compatible
ArchitecturesInstallIn64BitMode=x64compatible

DefaultDirName={autopf}\{#AppName}
DefaultGroupName={#AppName}

OutputDir=Output
OutputBaseFilename=SemiPlot-Setup

Compression=lzma2/ultra64
SolidCompression=yes
LZMAUseSeparateProcess=yes

WizardStyle=modern
SetupIconFile={#AppIconFile}
UninstallDisplayIcon={app}\{#AppExeName}

; Program Files and C:\DISTR both need admin rights
PrivilegesRequired=admin

CloseApplications=yes
CloseApplicationsFilter=*{#AppExeName}*

[Languages]
Name: "russian"; MessagesFile: "compiler:Languages\Russian.isl"
Name: "english"; MessagesFile: "compiler:Default.isl"

[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked

[Dirs]
; The operator, not the installing admin, writes both: the settings window and the log sink
Name: "{#ConfigRoot}"; Permissions: users-modify; Flags: uninsneveruninstall
Name: "{#LogRoot}"; Permissions: users-modify; Flags: uninsneveruninstall

[Files]
Source: "{#SrcBinDir}\*"; Excludes: "*.pdb"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs

; An upgrade keeps the operator's configuration, the archive password included
Source: "{#SrcCfgDir}\*"; DestDir: "{#ConfigRoot}"; Flags: onlyifdoesntexist uninsneveruninstall recursesubdirs createallsubdirs

[Icons]
Name: "{group}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "{#LaunchArgs}"
Name: "{group}\{cm:UninstallProgram,{#AppName}}"; Filename: "{uninstallexe}"
Name: "{autodesktop}\{#AppName}"; Filename: "{app}\{#AppExeName}"; Parameters: "{#LaunchArgs}"; Tasks: desktopicon

[Run]
Filename: "{app}\{#AppExeName}"; Parameters: "{#LaunchArgs}"; Description: "{cm:LaunchProgram,{#AppName}}"; Flags: nowait postinstall skipifsilent runasoriginaluser
1 change: 1 addition & 0 deletions SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<!-- Program.Main is the explicit entry point. -->
<StartupObject>SemiPlot.UI.Program</StartupObject>
<NeutralLanguage>en</NeutralLanguage>
<ApplicationIcon>logo.ico</ApplicationIcon>
</PropertyGroup>

<ItemGroup>
Expand Down
Binary file added SemiPlot/SemiPlot.UI/logo.ico
Binary file not shown.
12 changes: 12 additions & 0 deletions docs/architecture/overview.md
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,18 @@ one method: nothing else may resolve a service through a static.
in `C:\DISTR\Config\SemiPlot`, logs in `C:\DISTR\Logs\SemiPlot\`. Neither sits beside the
executable and neither is per-user. Nothing in the application knows those paths: the three launch
keys carry them, and the installer is what fills them in.
- **The installer.** `Installer/SemiPlot.iss` (Inno Setup) installs the published
`SemiPlot.UI` into `Program Files\SemiPlot` and writes the three launch keys into its shortcuts,
with `--logging-level information`. It grants `Users` modify on both site folders, because the
installing admin is not the operator the settings window and the log sink write as. It copies
`ConfigFiles/` only where a file does not exist yet, so an upgrade keeps the operator's password,
and the uninstaller leaves both folders in place. The cost: a key a later version makes required
never reaches an upgraded site, and its start fails naming the key. An unattended install runs
`SemiPlot-Setup.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART` and reads exit code 0 as success; a
deployment that knows the archive connection writes `connection/connection.yaml` under the
configuration folder before the installer runs, and the installer keeps it. `.github/workflows/release.yml`
builds the installer on a `vX.Y.Z` tag and publishes it with the tag annotation as the release body;
`release-tools:new` writes that annotation.
- **Configuration is a tree of section folders.** A section is one folder under `--config-dir`, and
every section folder behaves the same way: the loader reads every `*.yaml` in it, parses each file
on its own and merges them at the key level. A key carried by two files of one folder stops the
Expand Down
Loading