Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2c54840ba | |||
| 870426ec08 | |||
| 137929b4ac | |||
| 95d7599cb1 | |||
| 62254fcd8c | |||
| 7aa96c99a3 | |||
| 35455c2ef6 |
@@ -268,14 +268,47 @@ If the session is on the Linux dev server (`/home/chaim/espocrm-extensions/Outlo
|
||||
|
||||
---
|
||||
|
||||
## How releases work (current, 2026-05-24)
|
||||
|
||||
**Production install URL:** `https://platform.dev.marcus-law.co.il/outlook-addin/OutlookAddin.vsto`
|
||||
|
||||
**Release flow:**
|
||||
|
||||
1. Code change merged to `main` (push or PR-merge — no CI fires yet)
|
||||
2. Bump `<ApplicationVersion>` in `src/OutlookAddin/OutlookAddin.csproj` and `AssemblyVersion` + `AssemblyFileVersion` in `src/OutlookAddin/Properties/AssemblyInfo.cs` to the next 4-part version (e.g. `1.2.1.0`). CI overrides `ApplicationVersion` from the tag at build time, but leaves AssemblyInfo alone — keep them in sync manually so the bundled DLL's FileVersion matches the manifest.
|
||||
3. `git tag -a v1.2.1 -m "..." && git push --tags`
|
||||
4. Gitea Actions workflow (`.gitea/workflows/build.yml`) fires:
|
||||
- Imports the **real** codesign cert from secrets `CODESIGN_CERT_PFX_BASE64` + `CODESIGN_THUMBPRINT` (publicKeyToken `c68d2b4c25051c5b`, `CN=Marcus-Law OutlookAddin`)
|
||||
- Patches csproj to swap the dev self-signed thumbprint (`2399E9BF…`) for the real one
|
||||
- `msbuild /t:Publish` with `/p:InstallUrl=https://platform.dev.marcus-law.co.il/outlook-addin/` and `/p:ApplicationVersion=<tag>.0`
|
||||
- Uploads `publish.tar.gz` to Gitea generic registry (`outlook-addin-publish/<version>/publish.tar.gz`)
|
||||
- Linux job downloads the tar.gz, builds a Docker image `outlook-addin-cdn:latest`, pushes to Gitea registry, calls Coolify to redeploy the CDN container
|
||||
- Posts to Mattermost channel `git-verelasim` with the install URL on success / a failure link on failure
|
||||
5. Lawyers' installed Add-in checks the deployment manifest at the install URL every 7 days (configured in csproj `<UpdateInterval>7</UpdateInterval>`) or whenever they click "בדוק עדכונים" in Settings → אודות.
|
||||
|
||||
**Semver policy:** PATCH for bug fixes (unilateral), MINOR for visible features (unilateral when warranted), MAJOR only with Chaim's explicit approval.
|
||||
|
||||
**Branding (`KlearBranding` repo):** the product is presented to lawyers as **Klear** — title bars, MessageBox titles, ribbon group label, About header all say "Klear". The logo is mirrored from `espocrm-extensions/KlearBranding/files/client/custom/img/logo-klear.png` into `src/OutlookAddin.UI/Resources/logo-klear.png` and embedded as a WPF Resource. Brand accent color: `#121d2e` (dark navy).
|
||||
|
||||
**What stays MarcusLaw under the hood (deliberate — preserves auto-update across the rebrand):**
|
||||
- ClickOnce app identity (`AssemblyName=MarcusLaw.OutlookAddin`)
|
||||
- `%APPDATA%\MarcusLaw\OutlookAddin\settings.json` and `creds.dat`
|
||||
- `%LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs\`
|
||||
- `DpapiCredentialStore.EntropySeed = "MarcusLaw.OutlookAddin.v1"`
|
||||
- Code namespaces `MarcusLaw.OutlookAddin.*`
|
||||
- Named pipe `MarcusLaw.OutlookAddin`
|
||||
|
||||
Changing any of these would orphan all existing installs and require a coordinated uninstall+reinstall on every lawyer's PC plus an entropy migration for DPAPI-encrypted credentials. See the LegalCRM rebrand misadventure in git history for what *not* to do.
|
||||
|
||||
**Known traps when releasing:**
|
||||
- VS Publish wizard (Project Properties → Publish) **rewrites** csproj at every click — flips `<AutoIncrementApplicationRevision>` back to `true`, resets `<ApplicationVersion>` to whatever's in the UI, sometimes deletes prior `Application Files/<...>/` bundles. **Don't use it.** Tag and let CI do it.
|
||||
- There's an old `Publish/` directory in git history that was a parallel manual-release channel via `gitea.../raw/branch/main/Publish/`. It's **not the production install URL** — that's platform.dev. The folder was dropped in v1.2.0.
|
||||
|
||||
## Things deliberately deferred
|
||||
|
||||
| What | When | Why deferred |
|
||||
|---|---|---|
|
||||
| Windows CI runner on Coolify | Until ClickOnce publish becomes useful | Local builds on dev's VS 2022 suffice through week 5 |
|
||||
| Code signing cert (self-signed) + GPO push to 10 PCs | Week 6 (before release) | IT task; cert isn't useful until we have something to sign |
|
||||
| ClickOnce hosting setup at `platform.dev.marcus-law.co.il/outlook-addin/` | Week 6 | No build artifacts yet |
|
||||
| Mattermost release notification webhook in CI | Week 6 | Coupled to CI runner |
|
||||
| Identity-level rebrand to a pure Klear product (AssemblyName, namespaces, DPAPI entropy, AppData paths) | Until there's another reason to force-reinstall on all 10 PCs | Visible rebrand already done in v1.2.0 without disrupting users; full rename trades aesthetic cleanliness for a day of IT coordination + lost credentials |
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -123,10 +123,22 @@
|
||||
Foreground="{Binding UpdateStatusBrush}"
|
||||
TextWrapping="Wrap"
|
||||
Margin="0,4,0,0" />
|
||||
<TextBlock Margin="0,16,0,0" TextWrapping="Wrap"
|
||||
Text="לוגים: %LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs" />
|
||||
<TextBlock TextWrapping="Wrap"
|
||||
Text="הגדרות: %APPDATA%\MarcusLaw\OutlookAddin\settings.json" />
|
||||
<TextBlock Margin="0,16,0,0" TextWrapping="Wrap">
|
||||
<Run Text="לוגים: " />
|
||||
<Hyperlink Tag="%LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs"
|
||||
Click="OnOpenFolderHyperlink"
|
||||
ToolTip="פתח את התיקייה ב-Explorer">
|
||||
<Run Text="%LOCALAPPDATA%\MarcusLaw\OutlookAddin\logs" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
<TextBlock TextWrapping="Wrap">
|
||||
<Run Text="הגדרות: " />
|
||||
<Hyperlink Tag="%APPDATA%\MarcusLaw\OutlookAddin"
|
||||
Click="OnOpenFolderHyperlink"
|
||||
ToolTip="פתח את התיקייה ב-Explorer">
|
||||
<Run Text="%APPDATA%\MarcusLaw\OutlookAddin\settings.json" />
|
||||
</Hyperlink>
|
||||
</TextBlock>
|
||||
</StackPanel>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Windows;
|
||||
using System.Windows.Documents;
|
||||
using MarcusLaw.OutlookAddin.UI.ViewModels;
|
||||
|
||||
namespace MarcusLaw.OutlookAddin.UI.Dialogs
|
||||
@@ -40,5 +43,23 @@ namespace MarcusLaw.OutlookAddin.UI.Dialogs
|
||||
DialogResult = _viewModel?.DialogResult;
|
||||
Close();
|
||||
}
|
||||
|
||||
private void OnOpenFolderHyperlink(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (!(sender is Hyperlink h) || !(h.Tag is string raw)) return;
|
||||
try
|
||||
{
|
||||
var path = Environment.ExpandEnvironmentVariables(raw);
|
||||
// Create the folder lazily so the link works even on the very
|
||||
// first run, before any log has been written.
|
||||
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
|
||||
Process.Start(new ProcessStartInfo("explorer.exe", "\"" + path + "\"") { UseShellExecute = true });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show("פתיחת התיקייה נכשלה: " + ex.Message, "Klear",
|
||||
MessageBoxButton.OK, MessageBoxImage.Warning);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Threading.Tasks;
|
||||
using System.Windows.Media;
|
||||
@@ -178,18 +179,78 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
}
|
||||
|
||||
var deployment = System.Deployment.Application.ApplicationDeployment.CurrentDeployment;
|
||||
var info = await Task.Run(() => deployment.CheckForDetailedUpdate(false)).ConfigureAwait(true);
|
||||
if (!info.UpdateAvailable)
|
||||
var currentVersion = deployment.CurrentVersion;
|
||||
var manifestUri = deployment.UpdateLocation;
|
||||
|
||||
// ApplicationDeployment.CheckForDetailedUpdate triggers a
|
||||
// TrustManager re-evaluation which throws TrustNotGranted
|
||||
// inside a hosted VSTO process even when the publisher and
|
||||
// permissions are unchanged. Bypass it by fetching the
|
||||
// deployment manifest directly and reading the version.
|
||||
Version? availableVersion = null;
|
||||
try
|
||||
{
|
||||
UpdateStatusBrush = SuccessBrush;
|
||||
UpdateStatus = "אתה על הגרסה האחרונה (" + deployment.CurrentVersion + ").";
|
||||
availableVersion = await Task.Run(() => FetchManifestVersion(manifestUri)).ConfigureAwait(true);
|
||||
}
|
||||
catch (Exception fetchEx)
|
||||
{
|
||||
_logger.Warning(fetchEx, "CheckForUpdate: manifest fetch failed");
|
||||
UpdateStatusBrush = ErrorBrush;
|
||||
UpdateStatus = "שגיאה בבדיקת השרת: " + fetchEx.Message;
|
||||
return;
|
||||
}
|
||||
|
||||
UpdateStatus = $"זמינה גרסה {info.AvailableVersion} — מוריד…";
|
||||
var result = await Task.Run(() => deployment.Update()).ConfigureAwait(true);
|
||||
UpdateStatusBrush = SuccessBrush;
|
||||
UpdateStatus = $"הותקנה גרסה {info.AvailableVersion}. סגור ופתח את Outlook כדי לטעון אותה.";
|
||||
if (availableVersion == null)
|
||||
{
|
||||
UpdateStatusBrush = ErrorBrush;
|
||||
UpdateStatus = "לא ניתן לקרוא את גרסת השרת מ-" + manifestUri;
|
||||
return;
|
||||
}
|
||||
|
||||
if (availableVersion <= currentVersion)
|
||||
{
|
||||
UpdateStatusBrush = SuccessBrush;
|
||||
UpdateStatus = "אתה על הגרסה האחרונה (" + currentVersion + ").";
|
||||
return;
|
||||
}
|
||||
|
||||
// VSTO add-ins cannot be updated programmatically from inside
|
||||
// a running Outlook — ApplicationDeployment.Update() throws.
|
||||
// Don't shell-launch the .vsto URL either: Chrome / Edge will
|
||||
// download it to disk, and the resulting file has a different
|
||||
// security zone than the deployment manifest, which fails with
|
||||
// InvalidDeploymentException ("no matching security zones").
|
||||
// Call VSTOInstaller.exe directly so the URL is processed in
|
||||
// the Internet zone, matching the manifest.
|
||||
var installerPath = FindVstoInstaller();
|
||||
if (installerPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
System.Diagnostics.Process.Start(new System.Diagnostics.ProcessStartInfo(
|
||||
installerPath, "/I \"" + manifestUri + "\"")
|
||||
{
|
||||
UseShellExecute = false
|
||||
});
|
||||
UpdateStatusBrush = SuccessBrush;
|
||||
UpdateStatus = $"זמינה גרסה {availableVersion} — אשר את ההתקנה בחלון שנפתח, ואז סגור ופתח את Outlook.";
|
||||
}
|
||||
catch (Exception launchEx)
|
||||
{
|
||||
_logger.Warning(launchEx, "CheckForUpdate: VSTOInstaller launch failed");
|
||||
UpdateStatusBrush = ErrorBrush;
|
||||
UpdateStatus = $"זמינה גרסה {availableVersion} — סגור ופתח את Outlook כדי להתקין אוטומטית.";
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// VSTOInstaller not at the canonical path — VSTO runtime
|
||||
// still picks up new manifests on Outlook startup, so
|
||||
// ask the user to restart instead of attempting a hack.
|
||||
UpdateStatusBrush = SuccessBrush;
|
||||
UpdateStatus = $"זמינה גרסה {availableVersion} — סגור ופתח את Outlook כדי להתקין אוטומטית.";
|
||||
}
|
||||
return;
|
||||
}
|
||||
catch (System.Deployment.Application.DeploymentDownloadException ex)
|
||||
{
|
||||
@@ -215,6 +276,77 @@ namespace MarcusLaw.OutlookAddin.UI.ViewModels
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the top-level <assemblyIdentity> version from the
|
||||
/// .vsto deployment manifest at <paramref name="manifestUri"/>.
|
||||
/// Returns null if the document doesn't look like a deployment
|
||||
/// manifest. Runs on a worker thread (synchronous HTTP).
|
||||
/// </summary>
|
||||
private static Version? FetchManifestVersion(Uri manifestUri)
|
||||
{
|
||||
// The .vsto manifest is small (≈6KB). Keep this synchronous —
|
||||
// no point pulling in HttpClient and its lifecycle just for one
|
||||
// GET. WebRequest works fine on .NET FW 4.8 and through proxies.
|
||||
var req = System.Net.WebRequest.Create(manifestUri);
|
||||
req.Timeout = 10_000;
|
||||
using (var response = req.GetResponse())
|
||||
using (var stream = response.GetResponseStream())
|
||||
{
|
||||
var doc = System.Xml.Linq.XDocument.Load(stream);
|
||||
System.Xml.Linq.XNamespace asmv1 = "urn:schemas-microsoft-com:asm.v1";
|
||||
var identity = doc.Root?
|
||||
.Elements()
|
||||
.FirstOrDefault(e => e.Name.LocalName == "assemblyIdentity" && e.Name.Namespace == asmv1);
|
||||
var versionAttr = identity?.Attribute("version")?.Value;
|
||||
if (string.IsNullOrWhiteSpace(versionAttr)) return null;
|
||||
return Version.TryParse(versionAttr, out var v) ? v : null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Locates VSTOInstaller.exe on the machine. The path is stable
|
||||
/// for VSTO 4.0 (Office 2010+) — falls back to a registry lookup
|
||||
/// if the default install location was customised.
|
||||
/// </summary>
|
||||
private static string? FindVstoInstaller()
|
||||
{
|
||||
string[] candidates =
|
||||
{
|
||||
@"C:\Program Files (x86)\Common Files\Microsoft Shared\VSTO\10.0\VSTOInstaller.exe",
|
||||
@"C:\Program Files\Common Files\Microsoft Shared\VSTO\10.0\VSTOInstaller.exe"
|
||||
};
|
||||
foreach (var p in candidates)
|
||||
{
|
||||
if (System.IO.File.Exists(p)) return p;
|
||||
}
|
||||
|
||||
// Registry fallback: HKCR\VstoFile\shell\Open\command stores
|
||||
// the verb VSTOInstaller registers for the .vsto extension.
|
||||
try
|
||||
{
|
||||
using (var key = Microsoft.Win32.Registry.ClassesRoot.OpenSubKey(@"VstoFile\shell\Open\command"))
|
||||
{
|
||||
var cmd = key?.GetValue(null) as string;
|
||||
if (!string.IsNullOrEmpty(cmd))
|
||||
{
|
||||
// Value looks like: "C:\...\VSTOInstaller.exe" "%1"
|
||||
var firstQuote = cmd!.IndexOf('"');
|
||||
if (firstQuote == 0)
|
||||
{
|
||||
var endQuote = cmd.IndexOf('"', 1);
|
||||
if (endQuote > 1)
|
||||
{
|
||||
var exe = cmd.Substring(1, endQuote - 1);
|
||||
if (System.IO.File.Exists(exe)) return exe;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
catch { /* registry access denied or schema differs — fall through */ }
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool CanCheckForUpdate() => !IsCheckingForUpdate;
|
||||
|
||||
partial void OnIsCheckingForUpdateChanged(bool value)
|
||||
|
||||
@@ -36,7 +36,7 @@
|
||||
<PublishUrl>C:\Users\Chaim\source\repos\OutlookAddin\Publish\</PublishUrl>
|
||||
<InstallUrl>https://gitea.dev.marcus-law.co.il/espocrm-extensions/OutlookAddin/raw/branch/main/Publish/</InstallUrl>
|
||||
<TargetCulture>en</TargetCulture>
|
||||
<ApplicationVersion>1.2.0.0</ApplicationVersion>
|
||||
<ApplicationVersion>1.2.6.0</ApplicationVersion>
|
||||
<AutoIncrementApplicationRevision>false</AutoIncrementApplicationRevision>
|
||||
<UpdateEnabled>true</UpdateEnabled>
|
||||
<UpdateInterval>7</UpdateInterval>
|
||||
|
||||
@@ -33,6 +33,6 @@ using System.Security;
|
||||
// You can specify all the values or you can default the Build and Revision Numbers
|
||||
// by using the '*' as shown below:
|
||||
// [assembly: AssemblyVersion("1.0.*")]
|
||||
[assembly: AssemblyVersion("1.2.0.0")]
|
||||
[assembly: AssemblyFileVersion("1.2.0.0")]
|
||||
[assembly: AssemblyVersion("1.2.6.0")]
|
||||
[assembly: AssemblyFileVersion("1.2.6.0")]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user