From de2b15d89e41fbe0674e191c772ba610aa1c8b54 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 8 Oct 2025 09:15:37 +0200 Subject: [PATCH 1/3] Add workflow to create release discussions from labels --- .../label-to-release-announcement.yml | 163 ++++++++++++++++++ 1 file changed, 163 insertions(+) create mode 100644 .github/workflows/.github/workflows/label-to-release-announcement.yml diff --git a/.github/workflows/.github/workflows/label-to-release-announcement.yml b/.github/workflows/.github/workflows/label-to-release-announcement.yml new file mode 100644 index 0000000000..013b36d60c --- /dev/null +++ b/.github/workflows/.github/workflows/label-to-release-announcement.yml @@ -0,0 +1,163 @@ +name: Create a release discussions for each new version label + +on: + schedule: + - cron: "0 * * * *" # every hour + workflow_dispatch: # allow manual runs +permissions: + contents: read + discussions: write + issues: read + pull-requests: read + +jobs: + reconcile: + runs-on: ubuntu-latest + steps: + - name: Reconcile release/* labels → discussions + uses: actions/github-script@v7 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const categoryName = "Releases"; + + // 24h cutoff + const since = new Date(Date.now() - 24*60*60*1000).toISOString(); + core.info(`Scanning issues/PRs updated since ${since}`); + + // fetch repo + discussion categories + const repoData = await github.graphql(` + query($owner:String!, $repo:String!){ + repository(owner:$owner, name:$repo){ + id + discussionCategories(first:100){ nodes { id name } } + } + } + `, { owner, repo }); + const repoId = repoData.repository.id; + const category = repoData.repository.discussionCategories.nodes.find(c => c.name === categoryName); + if (!category) { + core.setFailed(`Discussion category "${categoryName}" not found`); + return; + } + const categoryId = category.id; + + // paginate issues/PRs updated in last 24h + for await (const { data: items } of github.paginate.iterator( + github.rest.issues.listForRepo, + { owner, repo, state: "all", since, per_page: 100 } + )) { + for (const item of items) { + const releaseLabels = (item.labels || []) + .map(l => (typeof l === "string" ? l : l.name)) // always get the name + .filter(n => typeof n === "string" && n.startsWith("release/")); + if (releaseLabels.length === 0) continue; + + core.info(`#${item.number}: ${releaseLabels.join(", ")}`); + + for (const labelName of releaseLabels) { + const version = labelName.substring("release/".length); + const titleTarget = `Release: ${version}`; + + // search discussions + let discussionId = null; + let cursor = null; + while (true) { + const page = await github.graphql(` + query($owner:String!, $repo:String!, $cursor:String){ + repository(owner:$owner, name:$repo){ + discussions(first:50, after:$cursor){ + nodes{ + id + title + url + category{ name } + labels(first:50){ nodes{ name } } + } + pageInfo{ hasNextPage endCursor } + } + } + } + `, { owner, repo, cursor }); + const nodes = page.repository.discussions.nodes; + const byLabel = nodes.find(d => + d.category?.name === categoryName && + d.labels?.nodes?.some(l => l.name === labelName) + ); + if (byLabel) { discussionId = byLabel.id; break; } + const byTitle = nodes.find(d => + d.category?.name === categoryName && + d.title === titleTarget + ); + if (byTitle) { discussionId = byTitle.id; break; } + if (!page.repository.discussions.pageInfo.hasNextPage) break; + cursor = page.repository.discussions.pageInfo.endCursor; + } + + if (!discussionId) { + core.info(`→ Creating discussion for ${labelName}`); + const body = + `**Release date:** TODO (YYYY-MM-DD)\n\n` + + `### Links\n` + + `- [Issues and pull requests marked for version ${version}](https://github.com/${owner}/${repo}/issues?q=label%3A${encodeURIComponent(labelName)})\n`; + + const created = await github.graphql(` + mutation($repoId:ID!, $catId:ID!, $title:String!, $body:String!){ + createDiscussion(input:{ + repositoryId:$repoId, + categoryId:$catId, + title:$title, + body:$body + }){ discussion{ id url } } + } + `, { repoId, catId: categoryId, title: titleTarget, body }); + + discussionId = created.createDiscussion.discussion.id; + + // lock the discussion to prevent replies + await github.graphql(` + mutation($id:ID!){ + lockLockable(input:{ lockableId:$id }) { + clientMutationId + } + } + `, { id: discussionId }); + core.info(`🔒 Locked discussion ${discussionId}`); + } else { + core.info(`→ Found existing discussion for ${labelName}`); + } + + // ensure label exists + let labelId; + try { + await github.rest.issues.getLabel({ owner, repo, name: labelName }); + } catch (e) { + if (e.status === 404) { + await github.rest.issues.createLabel({ + owner, repo, name: labelName, color: "0E8A16" + }); + } else { throw e; } + } + const labelNode = await github.graphql(` + query($owner:String!, $repo:String!, $name:String!){ + repository(owner:$owner, name:$repo){ label(name:$name){ id } } + } + `, { owner, repo, name: labelName }); + labelId = labelNode.repository.label?.id; + if (!labelId) continue; + + // add label to discussion + await github.graphql(` + mutation($id:ID!, $labels:[ID!]!){ + addLabelsToLabelable(input:{ labelableId:$id, labelIds:$labels }) { + clientMutationId + } + } + `, { id: discussionId, labels: [labelId] }); + + core.info(`✓ ${labelName} attached to discussion`); + } + } + } From adf8708987dcfc2b819eb7bd9a8739b8c30b84c5 Mon Sep 17 00:00:00 2001 From: Sebastiaan Janssen Date: Wed, 8 Oct 2025 09:17:43 +0200 Subject: [PATCH 2/3] Move yml file to the correct location --- .../{.github/workflows => }/label-to-release-announcement.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename .github/workflows/{.github/workflows => }/label-to-release-announcement.yml (100%) diff --git a/.github/workflows/.github/workflows/label-to-release-announcement.yml b/.github/workflows/label-to-release-announcement.yml similarity index 100% rename from .github/workflows/.github/workflows/label-to-release-announcement.yml rename to .github/workflows/label-to-release-announcement.yml From edf95e6fabbc25938d531cfd03986534587cbe31 Mon Sep 17 00:00:00 2001 From: Jacob Overgaard <752371+iOvergaard@users.noreply.github.com> Date: Wed, 8 Oct 2025 09:17:55 +0200 Subject: [PATCH 3/3] adds deprecation notices to `AllowNonExistingSegmentsCreation` --- .../Document/DocumentConfigurationResponseModel.cs | 1 + src/Umbraco.Core/Configuration/Models/SegmentSettings.cs | 1 + .../documents/workspace/document-workspace.context.ts | 9 +++++++++ 3 files changed, 11 insertions(+) diff --git a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs index 3fb63dadda..b4ef2374e7 100644 --- a/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs +++ b/src/Umbraco.Cms.Api.Management/ViewModels/Document/DocumentConfigurationResponseModel.cs @@ -8,5 +8,6 @@ public class DocumentConfigurationResponseModel public required bool AllowEditInvariantFromNonDefault { get; set; } + [Obsolete("This functionality will be moved to a client-side extension. Scheduled for removal in V19.")] public required bool AllowNonExistingSegmentsCreation { get; set; } } diff --git a/src/Umbraco.Core/Configuration/Models/SegmentSettings.cs b/src/Umbraco.Core/Configuration/Models/SegmentSettings.cs index e6fc90995a..9afc5004e4 100644 --- a/src/Umbraco.Core/Configuration/Models/SegmentSettings.cs +++ b/src/Umbraco.Core/Configuration/Models/SegmentSettings.cs @@ -19,5 +19,6 @@ public class SegmentSettings /// /// Gets or sets a value indicating whether the creation of non-existing segments is allowed. /// + [Obsolete("This functionality will be moved to a client-side extension. Scheduled for removal in V19.")] public bool AllowCreation { get; set; } = StaticAllowCreation; } diff --git a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts index 708fdc7d7e..857bc5b3c4 100644 --- a/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts +++ b/src/Umbraco.Web.UI.Client/src/packages/documents/documents/workspace/document-workspace.context.ts @@ -96,6 +96,15 @@ export class UmbDocumentWorkspaceContext const allowSegmentCreation = config?.allowNonExistingSegmentsCreation ?? false; const allowEditInvariantFromNonDefault = config?.allowEditInvariantFromNonDefault ?? true; + // Deprecation warning for allowNonExistingSegmentsCreation (default from server is true, so we warn on false) + if (!allowSegmentCreation) { + new UmbDeprecation({ + deprecated: 'The "AllowNonExistingSegmentsCreation" setting is deprecated.', + removeInVersion: '19.0.0', + solution: 'This functionality will be moved to a client-side extension.', + }).warn(); + } + this._variantOptionsFilter = (variantOption) => { const isNotCreatedSegmentVariant = variantOption.segment && !variantOption.variant;