From 9de5912d588ebee6498e2f80e20e89177f4b525d Mon Sep 17 00:00:00 2001 From: ToonCasteele <99814254+ToonCasteele@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:34:46 +0200 Subject: [PATCH 01/10] Promote release candidate deployment changes to main (#69) * Deploy release candidates separately (#66) * Add release candidate deployment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Configure release candidate deployment Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Deploy RC only after pull requests merge Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> * Keep production deploys limited to main Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/pull_request_template.md | 6 ++++ .github/workflows/deploy-dma-on-pr-merge.yml | 31 ++++++++++++++++---- README.md | 16 ++++++++++ 3 files changed, 48 insertions(+), 5 deletions(-) create mode 100644 .github/pull_request_template.md diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..36167a7 --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,6 @@ +## Target branch + +- [ ] This PR creates, updates, or deletes Dataverse data and targets `release-candidate`. +- [ ] This PR is read-only and targets `main`. + +Use the **base** selector on the pull request page before opening the PR. Promote the complete release candidate with a separate `release-candidate` to `main` pull request. diff --git a/.github/workflows/deploy-dma-on-pr-merge.yml b/.github/workflows/deploy-dma-on-pr-merge.yml index 70be2e2..6ff0e85 100644 --- a/.github/workflows/deploy-dma-on-pr-merge.yml +++ b/.github/workflows/deploy-dma-on-pr-merge.yml @@ -9,15 +9,18 @@ # DATAMINER_TOKEN, VITE_CLIENT_ID, VITE_TENANT_ID, VITE_DATAVERSE_URL # Required vars: # VITE_REDIRECT_URI -# Optional vars for manual dev deploy target: -# VITE_REDIRECT_URI_DEV +# Optional vars: +# VITE_REDIRECT_URI_DEV — Manual development deploy target +# VITE_REDIRECT_URI_RC — Release candidate deploy target -name: Build, Register and Deploy DMAPP to DMA on PR Merge +name: Deploy DynamicsActivities DMAPP on: pull_request: types: [closed] - branches: [main] + branches: + - main + - release-candidate workflow_dispatch: inputs: deployment_target: @@ -29,9 +32,16 @@ on: - production - dev +permissions: + contents: read + +concurrency: + group: deploy-dynamicsactivities-${{ github.ref }} + cancel-in-progress: false + jobs: deploy_production: - if: (github.event_name == 'pull_request' && github.event.pull_request.merged == true) || (github.event_name == 'workflow_dispatch' && github.event.inputs.deployment_target == 'production' && github.ref == 'refs/heads/main') + if: (github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main') || (github.event_name == 'workflow_dispatch' && github.event.inputs.deployment_target == 'production' && github.ref == 'refs/heads/main') uses: ./.github/workflows/deploy-dmapp-reusable.yml with: catalog_id: "b6dc92d4-401e-42fd-b7bd-87d1ff562420" @@ -41,6 +51,17 @@ jobs: webapp_public_folder: "DynamicsActivities" secrets: inherit + deploy_release_candidate: + if: github.event_name == 'pull_request' && github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'release-candidate' + uses: ./.github/workflows/deploy-dmapp-reusable.yml + with: + catalog_id: "b6dc92d4-401e-42fd-b7bd-87d1ff562420" + agent_destination_id: "e21dbc3e-7592-48f8-867b-d81d4ff06bee" + vite_redirect_uri: ${{ vars.VITE_REDIRECT_URI_RC }} + vite_app_base_path: "/public/DynamicsActivitiesRC/" + webapp_public_folder: "DynamicsActivitiesRC" + secrets: inherit + deploy_dev: if: github.event_name == 'workflow_dispatch' && github.event.inputs.deployment_target == 'dev' uses: ./.github/workflows/deploy-dmapp-reusable.yml diff --git a/README.md b/README.md index 6462636..a314aa9 100644 --- a/README.md +++ b/README.md @@ -221,6 +221,22 @@ and the frontend build base path is set to `/public/DynamicsActivitiesDev/` so s | `.github/workflows/copilot-bug-triage.yml` | Auto-comments on `bug`-labeled issues to ask `@copilot` for investigation | | `.github/workflows/sync-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR, enables auto-merge, asks `@copilot` to resolve conflicts, and retries when the PR is updated | +### Release candidate deployment + +`release-candidate` is the integration branch for all changes that create, update, or delete Dataverse data. Every push to it deploys the app to: + +```text +https://solutionsdma-skyline.on.dataminer.services/auth/?url=%2Fpublic%2FDynamicsActivitiesRC%2Findex.html +``` + +Open write-capable PRs with `release-candidate` selected as their base branch. When the candidate is approved, open a promotion PR from `release-candidate` into `main`; production remains deployed only when that PR merges. + +The release-candidate deployment uses the repository variable `VITE_REDIRECT_URI_RC`, which must match the redirect URI registered on the deployed Entra application: + +```text +https://solutionsdma-skyline.on.dataminer.services/public/DynamicsActivitiesRC/ +``` + ### Deployment URLs **Production** From fb152c066fb4f365d786bd0b4d50dd815b62b5f3 Mon Sep 17 00:00:00 2001 From: Bram De Block <104144721+BramDeBlock@users.noreply.github.com> Date: Thu, 16 Jul 2026 10:54:00 +0200 Subject: [PATCH 02/10] Revert "Merge pull request #62 from SkylineCommunications/feature/notification-subscription-system" This reverts commit e37817b34960bf364b25080bae9351646095afbe, reversing changes made to d75ba11c9b454474b9ab7475fee4a7720ec0e748. --- .github/copilot-instructions.md | 2 +- .../DynamicsActivitiesPackage.csproj | 4 +- .../DynamicsActivities_NotifySubscribers.cs | 104 +++++++----------- .../DynamicsActivities_Summarize.cs | 60 +--------- README.md | 2 +- 5 files changed, 49 insertions(+), 123 deletions(-) diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md index 071bdd8..7bc0166 100644 --- a/.github/copilot-instructions.md +++ b/.github/copilot-instructions.md @@ -45,7 +45,7 @@ The GitHub Actions workflow `Build, Register and Deploy DMAPP to DMA on PR Merge - `Frequency` (`id=10`, string): `instant`, `daily`, `weekly`, `monthly`. - `ClientSecret` (`id=11`, string): Dataverse client secret. - The notify script does **not** enforce an internal cadence gate; every run processes subscriptions matching the passed `Frequency`. -- Activity detection includes both new and updated activities: a record is included if `createdon > since` OR `modifiedon > since` per subscription. +- New activity detection uses `createdon > LastSentAt` per subscription. - Sender/from behavior is controlled by DataMiner mail/SMTP configuration. --- diff --git a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj index a5bff37..2f7d1ab 100644 --- a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj +++ b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj @@ -8,8 +8,8 @@ True 10.3.0.0 - 12752 - 1.8.86 - Include new and updated activities in email digest (createdon or modifiedon since last sent) + 1.8.85 + Fix sync workflow startup configuration skyline:sdk:dataminertoken skyline:sdk:dataminertoken diff --git a/DynamicsActivitiesPackage/DynamicsActivities_NotifySubscribers/DynamicsActivities_NotifySubscribers.cs b/DynamicsActivitiesPackage/DynamicsActivities_NotifySubscribers/DynamicsActivities_NotifySubscribers.cs index 4e78275..38b9b0e 100644 --- a/DynamicsActivitiesPackage/DynamicsActivities_NotifySubscribers/DynamicsActivities_NotifySubscribers.cs +++ b/DynamicsActivitiesPackage/DynamicsActivities_NotifySubscribers/DynamicsActivities_NotifySubscribers.cs @@ -132,7 +132,7 @@ private void RunSafe(IEngine engine) var activities = FetchActivities(scopeType, scopeValue, activityTypesJson, since, now); if (activities.Count == 0) { - engine.GenerateInformation($"[NotifySubscribers] No new or updated activities for sub {instance.ID.Id} ({scopeType}:{scopeValue}) since {since:o}. LastSentAt={lastSentAt:o}, CreatedAt={(createdAt.HasValue ? createdAt.Value.ToString("o") : "n/a")}."); + engine.GenerateInformation($"[NotifySubscribers] No new activities for sub {instance.ID.Id} ({scopeType}:{scopeValue}) since {since:o}. LastSentAt={lastSentAt:o}, CreatedAt={(createdAt.HasValue ? createdAt.Value.ToString("o") : "n/a")}."); continue; } @@ -191,7 +191,7 @@ private List FetchActivities(string scopeType, string scopeValue, var requestedTypes = ParseActivityTypes(activityTypesJson); var includeAllTypes = requestedTypes == null || requestedTypes.Count == 0; var activities = new List(); - var fromIso = BuildActivityChangeLowerBound(since); + var fromIso = BuildCreatedOnLowerBound(since); var escalationScope = String.Equals((scopeType ?? String.Empty).Trim(), "escalation", StringComparison.OrdinalIgnoreCase); if (escalationScope) @@ -212,8 +212,8 @@ private List FetchActivities(string scopeType, string scopeValue, return activities .Where(a => !string.IsNullOrEmpty(a.Id)) .GroupBy(a => a.Id) - .Select(g => g.OrderByDescending(GetActivityTimestamp).First()) - .OrderByDescending(GetActivityTimestamp) + .Select(g => g.OrderByDescending(i => i.CreatedOn).First()) + .OrderByDescending(i => i.CreatedOn) .ToList(); } @@ -248,8 +248,8 @@ private List FetchEscalationScopedActivities(string fromIso, bool return activities .Where(a => !string.IsNullOrEmpty(a.Id)) .GroupBy(a => a.Id) - .Select(g => g.OrderByDescending(GetActivityTimestamp).First()) - .OrderByDescending(GetActivityTimestamp) + .Select(g => g.OrderByDescending(i => i.CreatedOn).First()) + .OrderByDescending(i => i.CreatedOn) .ToList(); } @@ -386,7 +386,7 @@ private static string BuildOrFilter(string fieldName, List ids) return "(" + string.Join(" or ", valid.Select(id => $"{fieldName} eq {id}")) + ")"; } - private static string BuildActivityChangeLowerBound(DateTime since) + private static string BuildCreatedOnLowerBound(DateTime since) { // Dataverse does not reliably handle DateTime.MinValue in OData filters. // First-run should not enforce a lower bound. @@ -395,20 +395,15 @@ private static string BuildActivityChangeLowerBound(DateTime since) return since.ToUniversalTime().ToString("o", CultureInfo.InvariantCulture); } - private static void AddNewOrUpdatedFilter(List filters, string fromIso) + private List FetchStandardActivities(string entitySet, string typeLabel, string lookupField, List accountIds, string fromIso) { + var filters = BuildLookupFilters(lookupField, accountIds); if (!string.IsNullOrEmpty(fromIso)) { - filters.Add($"(createdon gt {fromIso} or modifiedon gt {fromIso})"); + filters.Add($"createdon gt {fromIso}"); } - } - - private List FetchStandardActivities(string entitySet, string typeLabel, string lookupField, List accountIds, string fromIso) - { - var filters = BuildLookupFilters(lookupField, accountIds); - AddNewOrUpdatedFilter(filters, fromIso); var filter = filters.Count > 0 ? "&$filter=" + string.Join(" and ", filters) : string.Empty; - var json = DataverseGet($"/{entitySet}?$select=activityid,subject,description,createdon,modifiedon,_regardingobjectid_value{filter}&$orderby=modifiedon desc&$top=100"); + var json = DataverseGet($"/{entitySet}?$select=activityid,subject,description,createdon,_regardingobjectid_value{filter}&$orderby=createdon desc&$top=100"); return json["value"]?.Select(v => new ActivityItem { @@ -418,7 +413,6 @@ private List FetchStandardActivities(string entitySet, string type Subject = v["subject"]?.Value(), Description = v["description"]?.Value(), CreatedOn = ParseDateTime(v["createdon"]?.Value()) ?? DateTime.MinValue, - ModifiedOn = ParseDateTime(v["modifiedon"]?.Value()) ?? DateTime.MinValue, RegardingId = v["_regardingobjectid_value"]?.Value(), Regarding = v["_regardingobjectid_value@OData.Community.Display.V1.FormattedValue"]?.Value(), }).ToList() ?? new List(); @@ -427,10 +421,13 @@ private List FetchStandardActivities(string entitySet, string type private List FetchStandardActivitiesLinkedToEscalations(string entitySet, string typeLabel, List escalationIds, string fromIso, Dictionary escalationAccountById) { var filters = new List(); - AddNewOrUpdatedFilter(filters, fromIso); + if (!string.IsNullOrEmpty(fromIso)) + { + filters.Add($"createdon gt {fromIso}"); + } var filter = filters.Count > 0 ? "&$filter=" + string.Join(" and ", filters) : string.Empty; - var values = DataverseGetAllValues($"/{entitySet}?$select=activityid,subject,description,createdon,modifiedon,_regardingobjectid_value{filter}&$orderby=modifiedon desc", 20); + var values = DataverseGetAllValues($"/{entitySet}?$select=activityid,subject,description,createdon,_regardingobjectid_value{filter}&$orderby=createdon desc", 20); var result = new List(); foreach (var v in values) @@ -454,7 +451,6 @@ private List FetchStandardActivitiesLinkedToEscalations(string ent Subject = v["subject"]?.Value(), Description = description, CreatedOn = ParseDateTime(v["createdon"]?.Value()) ?? DateTime.MinValue, - ModifiedOn = ParseDateTime(v["modifiedon"]?.Value()) ?? DateTime.MinValue, RegardingId = regardingId, Regarding = v["_regardingobjectid_value@OData.Community.Display.V1.FormattedValue"]?.Value(), }; @@ -475,9 +471,12 @@ private List FetchStandardActivitiesLinkedToEscalations(string ent private List FetchEscalations(List accountIds, string fromIso) { var filters = BuildLookupFilters(EscalationAccountLookupField, accountIds); - AddNewOrUpdatedFilter(filters, fromIso); + if (!string.IsNullOrEmpty(fromIso)) + { + filters.Add($"createdon gt {fromIso}"); + } var filter = filters.Count > 0 ? "&$filter=" + string.Join(" and ", filters) : string.Empty; - var json = DataverseGet($"/slc_escalations?$select=slc_escalationid,slc_name,createdon,modifiedon,{EscalationAccountLookupField},slc_startdate,statecode,statuscode{filter}&$orderby=modifiedon desc&$top=100"); + var json = DataverseGet($"/slc_escalations?$select=slc_escalationid,slc_name,createdon,{EscalationAccountLookupField},slc_startdate,statecode,statuscode{filter}&$orderby=createdon desc&$top=100"); return json["value"]?.Select(v => new ActivityItem { @@ -487,7 +486,6 @@ private List FetchEscalations(List accountIds, string from Subject = v["slc_name"]?.Value() ?? "Escalation", Description = String.Empty, CreatedOn = ParseDateTime(v["createdon"]?.Value()) ?? DateTime.MinValue, - ModifiedOn = ParseDateTime(v["modifiedon"]?.Value()) ?? DateTime.MinValue, RegardingId = NormalizeDataverseId(v[EscalationAccountLookupField]?.Value()), Regarding = v[$"{EscalationAccountLookupField}@OData.Community.Display.V1.FormattedValue"]?.Value(), }).ToList() ?? new List(); @@ -497,8 +495,8 @@ private List FetchLeads(List accountIds, string fromIso) { if (accountIds.Count == 0) return new List(); var parentFilter = "(" + string.Join(" or ", accountIds.Select(id => $"_parentaccountid_value eq {id}")) + ")"; - var lowerBound = !string.IsNullOrEmpty(fromIso) ? $" and (createdon gt {fromIso} or modifiedon gt {fromIso})" : string.Empty; - var json = DataverseGet($"/leads?$select=leadid,subject,description,createdon,modifiedon,_parentaccountid_value&$filter={parentFilter}{lowerBound}&$orderby=modifiedon desc&$top=100"); + var lowerBound = !string.IsNullOrEmpty(fromIso) ? $" and createdon gt {fromIso}" : string.Empty; + var json = DataverseGet($"/leads?$select=leadid,subject,description,createdon,_parentaccountid_value&$filter={parentFilter}{lowerBound}&$orderby=createdon desc&$top=100"); return json["value"]?.Select(v => new ActivityItem { @@ -508,7 +506,6 @@ private List FetchLeads(List accountIds, string fromIso) Subject = v["subject"]?.Value(), Description = v["description"]?.Value(), CreatedOn = ParseDateTime(v["createdon"]?.Value()) ?? DateTime.MinValue, - ModifiedOn = ParseDateTime(v["modifiedon"]?.Value()) ?? DateTime.MinValue, Regarding = v["_parentaccountid_value@OData.Community.Display.V1.FormattedValue"]?.Value(), }).ToList() ?? new List(); } @@ -517,8 +514,8 @@ private List FetchOpportunities(List accountIds, string fr { if (accountIds.Count == 0) return new List(); var parentFilter = "(" + string.Join(" or ", accountIds.Select(id => $"_parentaccountid_value eq {id}")) + ")"; - var lowerBound = !string.IsNullOrEmpty(fromIso) ? $" and (createdon gt {fromIso} or modifiedon gt {fromIso})" : string.Empty; - var json = DataverseGet($"/opportunities?$select=opportunityid,name,description,createdon,modifiedon,_parentaccountid_value,slc_opportunitytype&$filter={parentFilter}{lowerBound}&$orderby=modifiedon desc&$top=100"); + var lowerBound = !string.IsNullOrEmpty(fromIso) ? $" and createdon gt {fromIso}" : string.Empty; + var json = DataverseGet($"/opportunities?$select=opportunityid,name,description,createdon,_parentaccountid_value,slc_opportunitytype&$filter={parentFilter}{lowerBound}&$orderby=createdon desc&$top=100"); var result = new List(); foreach (var v in json["value"] ?? new JArray()) { @@ -534,7 +531,6 @@ private List FetchOpportunities(List accountIds, string fr Subject = v["name"]?.Value(), Description = v["description"]?.Value(), CreatedOn = ParseDateTime(v["createdon"]?.Value()) ?? DateTime.MinValue, - ModifiedOn = ParseDateTime(v["modifiedon"]?.Value()) ?? DateTime.MinValue, Regarding = v["_parentaccountid_value@OData.Community.Display.V1.FormattedValue"]?.Value(), }); } @@ -557,10 +553,13 @@ private List FetchAnnotations(ScopeContext scope, string fromIso) } var filters = new List(); - AddNewOrUpdatedFilter(filters, fromIso); + if (!string.IsNullOrEmpty(fromIso)) + { + filters.Add($"createdon gt {fromIso}"); + } var filter = filters.Count > 0 ? "&$filter=" + string.Join(" and ", filters) : string.Empty; - var values = DataverseGetAllValues($"/annotations?$select=annotationid,subject,notetext,createdon,modifiedon,_objectid_value{filter}&$orderby=modifiedon desc", 20); + var values = DataverseGetAllValues($"/annotations?$select=annotationid,subject,notetext,createdon,_objectid_value{filter}&$orderby=createdon desc", 20); var result = new List(); foreach (var v in values) { @@ -597,7 +596,6 @@ private List FetchAnnotations(ScopeContext scope, string fromIso) Subject = v["subject"]?.Value(), Description = v["notetext"]?.Value(), CreatedOn = ParseDateTime(v["createdon"]?.Value()) ?? DateTime.MinValue, - ModifiedOn = ParseDateTime(v["modifiedon"]?.Value()) ?? DateTime.MinValue, RegardingId = regardingId, Regarding = v["_objectid_value@OData.Community.Display.V1.FormattedValue"]?.Value(), }); @@ -698,10 +696,13 @@ private static bool ValueMatchesScope(string value, string scopeValue) private List FetchAnnotationsLinkedToEscalations(List escalationIds, string fromIso, Dictionary escalationAccountById) { var filters = new List(); - AddNewOrUpdatedFilter(filters, fromIso); + if (!string.IsNullOrEmpty(fromIso)) + { + filters.Add($"createdon gt {fromIso}"); + } var filter = filters.Count > 0 ? "&$filter=" + string.Join(" and ", filters) : string.Empty; - var values = DataverseGetAllValues($"/annotations?$select=annotationid,subject,notetext,createdon,modifiedon,_objectid_value{filter}&$orderby=modifiedon desc", 20); + var values = DataverseGetAllValues($"/annotations?$select=annotationid,subject,notetext,createdon,_objectid_value{filter}&$orderby=createdon desc", 20); var result = new List(); foreach (var v in values) @@ -723,7 +724,6 @@ private List FetchAnnotationsLinkedToEscalations(List esca Subject = v["subject"]?.Value(), Description = v["notetext"]?.Value(), CreatedOn = ParseDateTime(v["createdon"]?.Value()) ?? DateTime.MinValue, - ModifiedOn = ParseDateTime(v["modifiedon"]?.Value()) ?? DateTime.MinValue, RegardingId = regardingId, Regarding = v["_objectid_value@OData.Community.Display.V1.FormattedValue"]?.Value(), }; @@ -864,8 +864,6 @@ private static string BuildSummarizePayload(string scopeLabel, DateTime since, D .Select(activity => new JObject { ["createdOnUtc"] = activity.CreatedOn == DateTime.MinValue ? null : activity.CreatedOn.ToString("o", CultureInfo.InvariantCulture), - ["modifiedOnUtc"] = activity.ModifiedOn == DateTime.MinValue ? null : activity.ModifiedOn.ToString("o", CultureInfo.InvariantCulture), - ["changedOnUtc"] = GetActivityTimestamp(activity) == DateTime.MinValue ? null : GetActivityTimestamp(activity).ToString("o", CultureInfo.InvariantCulture), ["type"] = GetTypeBadgeLabel(activity), ["subject"] = String.IsNullOrWhiteSpace(activity.Subject) ? "(No subject)" : activity.Subject.Trim(), ["regarding"] = String.IsNullOrWhiteSpace(activity.Regarding) ? "-" : activity.Regarding.Trim(), @@ -1090,7 +1088,7 @@ private static string BuildFallbackDigestSummary(string scopeLabel, List $"{GetActivityTimestamp(activity):yyyy-MM-dd HH:mm} UTC — {GetTypeBadgeLabel(activity)} — {activity.Subject ?? "(No subject)"}") + .Select(activity => $"{activity.CreatedOn:yyyy-MM-dd HH:mm} UTC — {GetTypeBadgeLabel(activity)} — {activity.Subject ?? "(No subject)"}") .ToList(); var summary = new StringBuilder(); @@ -1132,7 +1130,7 @@ private static string BuildEmailBody(string userName, string scopeType, string s sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine($"
Scope: {HtmlEncode(scopeLabel ?? scopeValue)}
"); - sb.AppendLine($"
New/updated activities: {activities.Count} since {since:yyyy-MM-dd HH:mm} UTC
"); + sb.AppendLine($"
New activities: {activities.Count} since {since:yyyy-MM-dd HH:mm} UTC
"); sb.AppendLine(activityTypes != null && activityTypes.Count > 0 ? $"
Activity types: {HtmlEncode(string.Join(", ", activityTypes))}
" : "
Activity types: All
"); @@ -1157,7 +1155,7 @@ private static string BuildEmailBody(string userName, string scopeType, string s sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine($"{HtmlEncode(typeLabel)}"); - sb.AppendLine($"{GetActivityTimeLabel(item)}"); + sb.AppendLine($"{item.CreatedOn:yyyy-MM-dd HH:mm} UTC"); sb.AppendLine("
"); sb.AppendLine($"
{HtmlEncode(item.Subject ?? "(No subject)")}
"); sb.AppendLine($"
Regarding: {HtmlEncode(item.Regarding)}
"); @@ -1176,7 +1174,7 @@ private static string BuildEmailBody(string userName, string scopeType, string s sb.AppendLine("
"); } - sb.AppendLine("
You only receive entries created or updated since your previous digest for this subscription.
"); + sb.AppendLine("
You only receive entries newer than your previous digest for this subscription.
"); sb.AppendLine("
"); sb.AppendLine("
"); sb.AppendLine("You are receiving this because you subscribed to activity notifications in Dynamics Activities.
"); @@ -1398,29 +1396,6 @@ private static string GetTypeBadgeLabel(ActivityItem item) } } - private static DateTime GetActivityTimestamp(ActivityItem item) - { - if (item == null) return DateTime.MinValue; - if (item.ModifiedOn > DateTime.MinValue && item.ModifiedOn > item.CreatedOn) return item.ModifiedOn; - return item.CreatedOn; - } - - private static string GetActivityTimeLabel(ActivityItem item) - { - var timestamp = GetActivityTimestamp(item); - if (timestamp <= DateTime.MinValue) - { - return String.Empty; - } - - if (item?.ModifiedOn > DateTime.MinValue && item.ModifiedOn > item.CreatedOn) - { - return $"Updated {timestamp:yyyy-MM-dd HH:mm} UTC"; - } - - return $"{timestamp:yyyy-MM-dd HH:mm} UTC"; - } - private static string GetDynamicsUrl(string entityType, string activityId) { var etn = "activitypointer"; @@ -1463,7 +1438,6 @@ private sealed class ActivityItem public string RegardingId { get; set; } public string Regarding { get; set; } public DateTime CreatedOn { get; set; } - public DateTime ModifiedOn { get; set; } } private sealed class ScopeContext diff --git a/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs b/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs index 5f20587..b87759a 100644 --- a/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs +++ b/DynamicsActivitiesPackage/DynamicsActivities_Summarize/DynamicsActivities_Summarize.cs @@ -145,11 +145,11 @@ private static string BuildPrompt(SummaryRequest request) foreach (var activity in request.Activities.Take(50)) { var type = SafeValue(activity.Type); - var changedOn = SafeValue(GetActivityTimelineTimestampUtc(activity)); + var createdOn = SafeValue(activity.CreatedOnUtc); var regarding = SafeValue(activity.Regarding); var subject = SafeValue(activity.Subject); var description = TrimValue(SafeValue(activity.Description), 260); - lines.Add("- [" + changedOn + "] " + type + " | Regarding: " + regarding + " | Subject: " + subject + " | Note: " + description); + lines.Add("- [" + createdOn + "] " + type + " | Regarding: " + regarding + " | Subject: " + subject + " | Note: " + description); } return String.Join(Environment.NewLine, lines); @@ -309,8 +309,6 @@ private static SummaryRequest ParseSummaryRequest(string payload) request.Activities.Add(new ActivityInput { CreatedOnUtc = item["createdOnUtc"]?.Value(), - ModifiedOnUtc = item["modifiedOnUtc"]?.Value(), - ChangedOnUtc = item["changedOnUtc"]?.Value(), Type = item["type"]?.Value(), Subject = item["subject"]?.Value(), Regarding = item["regarding"]?.Value(), @@ -318,10 +316,6 @@ private static SummaryRequest ParseSummaryRequest(string payload) }); } - request.Activities = request.Activities - .OrderByDescending(GetActivityTimelineTimestampSortKey) - .ToList(); - return request; } @@ -382,7 +376,7 @@ private static string BuildFallbackSummary(SummaryRequest request) var topItems = activities .Take(3) - .Select(a => "[" + SafeValue(GetActivityTimelineTimestampUtc(a)) + "] " + SafeValue(a.Type) + ": " + SafeValue(a.Subject)) + .Select(a => "[" + SafeValue(a.CreatedOnUtc) + "] " + SafeValue(a.Type) + ": " + SafeValue(a.Subject)) .ToList(); var sb = new StringBuilder(); @@ -436,15 +430,15 @@ private static string BuildSummaryHtml(string summary, SummaryRequest request) sb.Append("'>"); foreach (var activity in topActivities) { - var changedOn = SafeValue(GetActivityTimelineTimestampUtc(activity)); - var hasKnownTimestamp = !String.Equals(changedOn, "-", StringComparison.Ordinal); + var createdOn = SafeValue(activity.CreatedOnUtc); + var hasKnownTimestamp = !String.Equals(createdOn, "-", StringComparison.Ordinal); sb.Append("
  • "); if (hasKnownTimestamp) { sb.Append("["); - sb.Append(HtmlEncode(changedOn)); + sb.Append(HtmlEncode(createdOn)); sb.Append("] "); } sb.Append(HtmlEncode(SafeValue(activity.Type))); @@ -574,42 +568,6 @@ private static string TrimValue(string value, int maxLength) return value.Substring(0, maxLength) + "..."; } - private static string GetActivityTimelineTimestampUtc(ActivityInput activity) - { - if (activity == null) - { - return null; - } - - if (!String.IsNullOrWhiteSpace(activity.ChangedOnUtc)) - { - return activity.ChangedOnUtc; - } - - if (!String.IsNullOrWhiteSpace(activity.ModifiedOnUtc)) - { - return activity.ModifiedOnUtc; - } - - return activity.CreatedOnUtc; - } - - private static DateTime GetActivityTimelineTimestampSortKey(ActivityInput activity) - { - var raw = GetActivityTimelineTimestampUtc(activity); - if (String.IsNullOrWhiteSpace(raw)) - { - return DateTime.MinValue; - } - - if (DateTime.TryParseExact(raw, "o", CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind, out var parsed)) - { - return parsed; - } - - return DateTime.MinValue; - } - private sealed class SummaryRequest { [JsonProperty("scopeLabel")] @@ -630,12 +588,6 @@ private sealed class ActivityInput [JsonProperty("createdOnUtc")] public string CreatedOnUtc { get; set; } - [JsonProperty("modifiedOnUtc")] - public string ModifiedOnUtc { get; set; } - - [JsonProperty("changedOnUtc")] - public string ChangedOnUtc { get; set; } - [JsonProperty("type")] public string Type { get; set; } diff --git a/README.md b/README.md index a314aa9..8224cda 100644 --- a/README.md +++ b/README.md @@ -103,7 +103,7 @@ Subscriptions are fully **DataMiner-native** in the current implementation. Behavior to be aware of: - The notify script has **no internal cadence gate**; each run processes subscriptions matching the requested frequency -- Activity detection includes both new and updated activities: a record is included if `createdon > since` OR `modifiedon > since` +- New activity detection uses `createdon > LastSentAt` - Digest emails can include Assistant-generated HTML timeline summaries, with deterministic fallback when Assistant integration is unavailable - Sender/from behavior is controlled by DataMiner mail / SMTP configuration From 9c6b22de71b94f75c2b67b087f583cdeaca5598d Mon Sep 17 00:00:00 2001 From: ToonCasteele <99814254+ToonCasteele@users.noreply.github.com> Date: Fri, 17 Jul 2026 10:46:44 +0200 Subject: [PATCH 03/10] Fix main-to-release-candidate workflow trigger (#75) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sync-main-to-release-candidate.yml | 4 ---- .../DynamicsActivitiesPackage.csproj | 4 ++-- README.md | 2 +- 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.github/workflows/sync-main-to-release-candidate.yml b/.github/workflows/sync-main-to-release-candidate.yml index 0d5be09..852a2a8 100644 --- a/.github/workflows/sync-main-to-release-candidate.yml +++ b/.github/workflows/sync-main-to-release-candidate.yml @@ -3,9 +3,6 @@ name: Sync main to release candidate on: push: branches: [main] - pull_request: - types: [opened, reopened, synchronize] - branches: [release-candidate] workflow_dispatch: {} permissions: @@ -19,7 +16,6 @@ concurrency: jobs: sync: - if: ${{ github.event_name != 'pull_request' || github.event.pull_request.head.ref == 'main' }} runs-on: ubuntu-latest steps: - name: Create or reuse promotion pull request diff --git a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj index 2f7d1ab..0d20576 100644 --- a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj +++ b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj @@ -8,8 +8,8 @@ True 10.3.0.0 - 12752 - 1.8.85 - Fix sync workflow startup configuration + 1.8.86 + Trigger main-to-release-candidate sync only on main updates skyline:sdk:dataminertoken skyline:sdk:dataminertoken diff --git a/README.md b/README.md index 8224cda..8f2c897 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ and the frontend build base path is set to `/public/DynamicsActivitiesDev/` so s | `.github/workflows/deploy-dma-on-pr-merge.yml` | Caller workflow for production-on-merge, manual `production` from `main` only, and manual `dev` deploys | | `.github/workflows/deploy-dmapp-reusable.yml` | Reusable build/register/deploy workflow for DMAPP packaging and Catalog deployment | | `.github/workflows/copilot-bug-triage.yml` | Auto-comments on `bug`-labeled issues to ask `@copilot` for investigation | -| `.github/workflows/sync-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR, enables auto-merge, asks `@copilot` to resolve conflicts, and retries when the PR is updated | +| `.github/workflows/sync-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR after changes reach `main`, enables auto-merge, and asks `@copilot` to resolve conflicts | ### Release candidate deployment From 71e0d69009dca331a573e34bb0eb407957c70014 Mon Sep 17 00:00:00 2001 From: ToonCasteele <99814254+ToonCasteele@users.noreply.github.com> Date: Fri, 17 Jul 2026 11:39:04 +0200 Subject: [PATCH 04/10] Prevent sync workflow runs on PR updates (#76) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/sync-main-to-release-candidate.yml | 4 +++- .../DynamicsActivitiesPackage.csproj | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.github/workflows/sync-main-to-release-candidate.yml b/.github/workflows/sync-main-to-release-candidate.yml index 852a2a8..adfc964 100644 --- a/.github/workflows/sync-main-to-release-candidate.yml +++ b/.github/workflows/sync-main-to-release-candidate.yml @@ -1,7 +1,8 @@ name: Sync main to release candidate on: - push: + pull_request: + types: [closed] branches: [main] workflow_dispatch: {} @@ -16,6 +17,7 @@ concurrency: jobs: sync: + if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true runs-on: ubuntu-latest steps: - name: Create or reuse promotion pull request diff --git a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj index 0d20576..5b82e95 100644 --- a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj +++ b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj @@ -8,8 +8,8 @@ True 10.3.0.0 - 12752 - 1.8.86 - Trigger main-to-release-candidate sync only on main updates + 1.8.87 + Prevent sync workflow runs on pull request updates skyline:sdk:dataminertoken skyline:sdk:dataminertoken From e30fc4fd23257f72c3c70cf82f6428f072c425e9 Mon Sep 17 00:00:00 2001 From: ToonCasteele <99814254+ToonCasteele@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:14:45 +0200 Subject: [PATCH 05/10] Fix main-to-release-candidate workflow trigger (#77) Use the target branch workflow for merged main pull requests and keep manual sync runs on main only. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- ...se-candidate.yml => promote-main-to-release-candidate.yml} | 4 ++-- .../DynamicsActivitiesPackage.csproj | 4 ++-- README.md | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) rename .github/workflows/{sync-main-to-release-candidate.yml => promote-main-to-release-candidate.yml} (93%) diff --git a/.github/workflows/sync-main-to-release-candidate.yml b/.github/workflows/promote-main-to-release-candidate.yml similarity index 93% rename from .github/workflows/sync-main-to-release-candidate.yml rename to .github/workflows/promote-main-to-release-candidate.yml index adfc964..620f83a 100644 --- a/.github/workflows/sync-main-to-release-candidate.yml +++ b/.github/workflows/promote-main-to-release-candidate.yml @@ -1,7 +1,7 @@ name: Sync main to release candidate on: - pull_request: + pull_request_target: types: [closed] branches: [main] workflow_dispatch: {} @@ -17,7 +17,7 @@ concurrency: jobs: sync: - if: github.event_name == 'workflow_dispatch' || github.event.pull_request.merged == true + if: (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') runs-on: ubuntu-latest steps: - name: Create or reuse promotion pull request diff --git a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj index 5b82e95..0897f2a 100644 --- a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj +++ b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj @@ -8,8 +8,8 @@ True 10.3.0.0 - 12752 - 1.8.87 - Prevent sync workflow runs on pull request updates + 1.8.88 + Use target-branch workflow for main-to-release-candidate sync skyline:sdk:dataminertoken skyline:sdk:dataminertoken diff --git a/README.md b/README.md index 8f2c897..e171685 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ and the frontend build base path is set to `/public/DynamicsActivitiesDev/` so s | `.github/workflows/deploy-dma-on-pr-merge.yml` | Caller workflow for production-on-merge, manual `production` from `main` only, and manual `dev` deploys | | `.github/workflows/deploy-dmapp-reusable.yml` | Reusable build/register/deploy workflow for DMAPP packaging and Catalog deployment | | `.github/workflows/copilot-bug-triage.yml` | Auto-comments on `bug`-labeled issues to ask `@copilot` for investigation | -| `.github/workflows/sync-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR after changes reach `main`, enables auto-merge, and asks `@copilot` to resolve conflicts | +| `.github/workflows/promote-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR after changes reach `main`, enables auto-merge, and asks `@copilot` to resolve conflicts | ### Release candidate deployment From 35178ec5b5b0a55f3bf0f9a15495a343c6a2c4d6 Mon Sep 17 00:00:00 2001 From: ToonCasteele <99814254+ToonCasteele@users.noreply.github.com> Date: Fri, 17 Jul 2026 12:18:44 +0200 Subject: [PATCH 06/10] Run promotion sync on main pushes (#78) Trigger the promotion workflow on main pushes and keep manual dispatches restricted to main. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .github/workflows/promote-main-to-release-candidate.yml | 5 ++--- .../DynamicsActivitiesPackage.csproj | 4 ++-- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/.github/workflows/promote-main-to-release-candidate.yml b/.github/workflows/promote-main-to-release-candidate.yml index 620f83a..8384af9 100644 --- a/.github/workflows/promote-main-to-release-candidate.yml +++ b/.github/workflows/promote-main-to-release-candidate.yml @@ -1,8 +1,7 @@ name: Sync main to release candidate on: - pull_request_target: - types: [closed] + push: branches: [main] workflow_dispatch: {} @@ -17,7 +16,7 @@ concurrency: jobs: sync: - if: (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true) || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') runs-on: ubuntu-latest steps: - name: Create or reuse promotion pull request diff --git a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj index 0897f2a..2d7e676 100644 --- a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj +++ b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj @@ -8,8 +8,8 @@ True 10.3.0.0 - 12752 - 1.8.88 - Use target-branch workflow for main-to-release-candidate sync + 1.8.89 + Run promotion sync on main pushes skyline:sdk:dataminertoken skyline:sdk:dataminertoken From d110d82c1778ae0969d41fd5c4584ff518cdfffd Mon Sep 17 00:00:00 2001 From: ToonCasteele <99814254+ToonCasteele@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:04:10 +0200 Subject: [PATCH 07/10] Refresh promotion workflow registration (#79) Use explicit workflow dispatch and job expressions so GitHub registers the promotion workflow correctly. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../promote-main-to-release-candidate.yml | 16 ++++++++-------- .../DynamicsActivitiesPackage.csproj | 4 ++-- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/promote-main-to-release-candidate.yml b/.github/workflows/promote-main-to-release-candidate.yml index 8384af9..409f531 100644 --- a/.github/workflows/promote-main-to-release-candidate.yml +++ b/.github/workflows/promote-main-to-release-candidate.yml @@ -1,9 +1,9 @@ -name: Sync main to release candidate +name: Promote main to release candidate on: push: branches: [main] - workflow_dispatch: {} + workflow_dispatch: permissions: contents: write @@ -16,7 +16,7 @@ concurrency: jobs: sync: - if: github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') + if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }} runs-on: ubuntu-latest steps: - name: Create or reuse promotion pull request @@ -36,7 +36,7 @@ jobs: --base release-candidate \ --head main \ --title "Sync main into release-candidate" \ - --body $'Automated promotion of the latest main branch changes into release-candidate.\n\nThis pull request is maintained by the Sync main to release candidate workflow.')" + --body $'Automated promotion of the latest main branch changes into release-candidate.\n\nThis pull request is maintained by the Promote main to release candidate workflow.')" fi echo "number=$pr_number" >> "$GITHUB_OUTPUT" @@ -72,11 +72,11 @@ jobs: --paginate \ --jq '.[].body' | grep -Fq "$marker"; then gh pr comment "$PR_NUMBER" --body "$(cat <<'EOF' - -@copilot Please resolve the merge conflicts in this pull request between `main` and `release-candidate`. + + @copilot Please resolve the merge conflicts in this pull request between `main` and `release-candidate`. -Preserve the intended changes from both branches, run the repository's relevant checks, and push the conflict resolution to this pull request's head branch. Do not merge the pull request yourself; leave it ready for this workflow to auto-merge once it is clean. -EOF + Preserve the intended changes from both branches, run the repository's relevant checks, and push the conflict resolution to this pull request's head branch. Do not merge the pull request yourself; leave it ready for this workflow to auto-merge once it is clean. + EOF )" fi diff --git a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj index 2d7e676..1b05613 100644 --- a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj +++ b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj @@ -8,8 +8,8 @@ True 10.3.0.0 - 12752 - 1.8.89 - Run promotion sync on main pushes + 1.8.90 + Refresh promotion workflow registration skyline:sdk:dataminertoken skyline:sdk:dataminertoken From 36e2757d267de208c5d8fa6286c8635b0bf765a4 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:24:01 +0200 Subject: [PATCH 08/10] Fix: gh CLI fails with "not a git repository" in promote-main-to-release-candidate workflow (#80) * Initial plan * Fix sync job: add GH_REPO env var so gh CLI resolves repo without checkout --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/workflows/promote-main-to-release-candidate.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/promote-main-to-release-candidate.yml b/.github/workflows/promote-main-to-release-candidate.yml index 409f531..c015598 100644 --- a/.github/workflows/promote-main-to-release-candidate.yml +++ b/.github/workflows/promote-main-to-release-candidate.yml @@ -23,6 +23,7 @@ jobs: id: promotion env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} run: | pr_number="$(gh pr list \ --base release-candidate \ @@ -45,6 +46,7 @@ jobs: id: merge env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} PR_NUMBER: ${{ steps.promotion.outputs.number }} run: | merge_state="$(gh pr view "$PR_NUMBER" --json mergeStateStatus --jq '.mergeStateStatus')" @@ -60,6 +62,7 @@ jobs: if: steps.merge.outputs.conflicting == 'true' || failure() env: GH_TOKEN: ${{ github.token }} + GH_REPO: ${{ github.repository }} PR_NUMBER: ${{ steps.promotion.outputs.number }} run: | merge_state="$(gh pr view "$PR_NUMBER" --json mergeStateStatus --jq '.mergeStateStatus')" From 3157b75b9903da34755988c78962561bd076f46f Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:43:53 +0200 Subject: [PATCH 09/10] ci: fix sync job failing when release-candidate has no branch protection rules (#82) * Initial plan * ci: fall back to direct merge when auto-merge requires branch protection --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> --- .github/workflows/promote-main-to-release-candidate.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/promote-main-to-release-candidate.yml b/.github/workflows/promote-main-to-release-candidate.yml index c015598..5e81e33 100644 --- a/.github/workflows/promote-main-to-release-candidate.yml +++ b/.github/workflows/promote-main-to-release-candidate.yml @@ -56,7 +56,10 @@ jobs: exit 0 fi - gh pr merge "$PR_NUMBER" --auto --merge + if ! gh pr merge "$PR_NUMBER" --auto --merge 2>/dev/null; then + # Auto-merge requires branch protection rules; fall back to direct merge + gh pr merge "$PR_NUMBER" --merge + fi - name: Ask Copilot to resolve merge conflicts if: steps.merge.outputs.conflicting == 'true' || failure() From 2ced7497c41d6c0f049e265b2b149761a2ed6f61 Mon Sep 17 00:00:00 2001 From: Copilot <198982749+Copilot@users.noreply.github.com> Date: Fri, 17 Jul 2026 13:59:46 +0200 Subject: [PATCH 10/10] Use dedicated branch for promotion conflict resolution (#83) * Use dedicated promotion branch * Run promotion after conflict resolution * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Keep promotion branch current safely --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: ToonCasteele <99814254+ToonCasteele@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../promote-main-to-release-candidate.yml | 32 +++++++++++++++---- .../DynamicsActivitiesPackage.csproj | 4 +-- README.md | 2 +- 3 files changed, 29 insertions(+), 9 deletions(-) diff --git a/.github/workflows/promote-main-to-release-candidate.yml b/.github/workflows/promote-main-to-release-candidate.yml index 5e81e33..7d5b39e 100644 --- a/.github/workflows/promote-main-to-release-candidate.yml +++ b/.github/workflows/promote-main-to-release-candidate.yml @@ -3,6 +3,9 @@ name: Promote main to release candidate on: push: branches: [main] + pull_request_target: + types: [synchronize] + branches: [release-candidate] workflow_dispatch: permissions: @@ -16,7 +19,12 @@ concurrency: jobs: sync: - if: ${{ github.event_name == 'push' || (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') }} + if: >- + github.event_name == 'push' || + (github.event_name == 'workflow_dispatch' && github.ref == 'refs/heads/main') || + (github.event_name == 'pull_request_target' && + github.event.pull_request.head.ref == 'automation/main-to-release-candidate' && + github.event.pull_request.head.repo.full_name == github.repository) runs-on: ubuntu-latest steps: - name: Create or reuse promotion pull request @@ -25,9 +33,21 @@ jobs: GH_TOKEN: ${{ github.token }} GH_REPO: ${{ github.repository }} run: | + promotion_branch="automation/main-to-release-candidate" + main_sha="$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/heads/main" --jq '.object.sha')" + if ! gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f "ref=refs/heads/${promotion_branch}" \ + -f "sha=${main_sha}"; then + if ! gh api --method PATCH "repos/${GITHUB_REPOSITORY}/git/refs/heads/${promotion_branch}" \ + -f "sha=${main_sha}" \ + -F force=false; then + echo "Promotion branch has diverged from main; leaving it unchanged for conflict resolution." + fi + fi + pr_number="$(gh pr list \ --base release-candidate \ - --head main \ + --head "$promotion_branch" \ --state open \ --json number \ --jq '.[0].number // empty')" @@ -35,9 +55,9 @@ jobs: if [[ -z "$pr_number" ]]; then pr_number="$(gh pr create \ --base release-candidate \ - --head main \ + --head "$promotion_branch" \ --title "Sync main into release-candidate" \ - --body $'Automated promotion of the latest main branch changes into release-candidate.\n\nThis pull request is maintained by the Promote main to release candidate workflow.')" + --body $'Automated promotion of the latest main branch changes into release-candidate.\n\nThis pull request is maintained by the Promote main to release candidate workflow. Its dedicated branch allows Copilot to resolve conflicts without modifying the default branch.')" fi echo "number=$pr_number" >> "$GITHUB_OUTPUT" @@ -79,9 +99,9 @@ jobs: --jq '.[].body' | grep -Fq "$marker"; then gh pr comment "$PR_NUMBER" --body "$(cat <<'EOF' - @copilot Please resolve the merge conflicts in this pull request between `main` and `release-candidate`. + @copilot Please resolve the merge conflicts in this pull request. The head branch contains the latest `main` changes and the base is `release-candidate`. - Preserve the intended changes from both branches, run the repository's relevant checks, and push the conflict resolution to this pull request's head branch. Do not merge the pull request yourself; leave it ready for this workflow to auto-merge once it is clean. + Preserve the intended changes from both branches, run the repository's relevant checks, and push the conflict resolution to this pull request's dedicated head branch. Do not merge the pull request yourself; leave it ready for this workflow to auto-merge once it is clean. EOF )" fi diff --git a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj index 1b05613..8a50cf2 100644 --- a/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj +++ b/DynamicsActivitiesPackage/DynamicsActivitiesPackage/DynamicsActivitiesPackage.csproj @@ -8,8 +8,8 @@ True 10.3.0.0 - 12752 - 1.8.90 - Refresh promotion workflow registration + 1.8.91 + Use a dedicated branch for promotion conflict resolution skyline:sdk:dataminertoken skyline:sdk:dataminertoken diff --git a/README.md b/README.md index e171685..0329a1f 100644 --- a/README.md +++ b/README.md @@ -219,7 +219,7 @@ and the frontend build base path is set to `/public/DynamicsActivitiesDev/` so s | `.github/workflows/deploy-dma-on-pr-merge.yml` | Caller workflow for production-on-merge, manual `production` from `main` only, and manual `dev` deploys | | `.github/workflows/deploy-dmapp-reusable.yml` | Reusable build/register/deploy workflow for DMAPP packaging and Catalog deployment | | `.github/workflows/copilot-bug-triage.yml` | Auto-comments on `bug`-labeled issues to ask `@copilot` for investigation | -| `.github/workflows/promote-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR after changes reach `main`, enables auto-merge, and asks `@copilot` to resolve conflicts | +| `.github/workflows/promote-main-to-release-candidate.yml` | Opens or updates the `main` → `release-candidate` promotion PR after changes reach `main`, using a dedicated branch so `@copilot` can resolve conflicts without modifying `main` | ### Release candidate deployment