diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..f00ce91 --- /dev/null +++ b/.github/workflows/release.yml @@ -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<> "$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 diff --git a/Installer/SemiPlot.iss b/Installer/SemiPlot.iss new file mode 100644 index 0000000..4ead533 --- /dev/null +++ b/Installer/SemiPlot.iss @@ -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 diff --git a/SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj b/SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj index 7316238..955ef25 100644 --- a/SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj +++ b/SemiPlot/SemiPlot.UI/SemiPlot.UI.csproj @@ -5,6 +5,7 @@ SemiPlot.UI.Program en + logo.ico diff --git a/SemiPlot/SemiPlot.UI/logo.ico b/SemiPlot/SemiPlot.UI/logo.ico new file mode 100644 index 0000000..78eb37b Binary files /dev/null and b/SemiPlot/SemiPlot.UI/logo.ico differ diff --git a/docs/architecture/overview.md b/docs/architecture/overview.md index 7e96b42..39ed17a 100644 --- a/docs/architecture/overview.md +++ b/docs/architecture/overview.md @@ -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