From e8b049338a639cb330b0249f23e65f6f343efd83 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Sun, 30 Aug 2026 20:45:19 +0200 Subject: [PATCH 1/4] IONOS(ci): gate the remote trigger on REMOTE_TRIGGER_NC_VERSION / REMOTE_TRIGGER_RC_BRANCH MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Nextcloud-major freeze was hard-coded in `trigger-remote-dev-workflow`'s `if:` as exact-equality arms on `ionos-dev`/`ionos-stable`: the per-major lanes appeared in `on: push: branches` and in the upload-to-artifactory gate, but could never trigger a remote deploy. Moving the deployed major therefore meant editing this workflow — on every lane's own copy of it, since GitHub reads the `on:`/`if:` blocks only from the pushed branch's copy. Port nc-server's variable-driven gate (IONOS-Productivity/nc-server 01816be5fdd) so the freeze lives in repository variables instead: - bare `ionos-dev`/`ionos-stable` are always exempt - `ionos-(dev|stable)-v*` must end with REMOTE_TRIGGER_NC_VERSION - `rc/*` must equal REMOTE_TRIGGER_RC_BRANCH exactly - `*/dev/*` bypasses the version gate - REMOTE_TRIGGER_NC_VERSION unset means all versions are allowed The suffix check is deliberately scoped to the `ionos-*-v*` lanes. Applied to `rc/*` it would admit any RC branch whose name happened to end with the whitelisted version string, bypassing the exact RC whitelist — and ncw's current train, `rc/ncw-7`, encodes no major at all. Behaviour-neutral as configured. With REMOTE_TRIGGER_NC_VERSION='v31' and REMOTE_TRIGGER_RC_BRANCH='rc/ncw-7', ionos-dev (NC31), ionos-stable and rc/ncw-7 trigger exactly as before and ionos-dev-v32 (NC32) stays frozen — now because v32 is not the whitelisted major rather than because the YAML excludes it. DISABLE_REMOTE_TRIGGER='true' is set repo-wide, so this lands inert regardless. The BUILD_TYPE stable arm has to widen in the same commit: suffixed stable lanes can now reach the job and would otherwise deploy as BUILD_TYPE=dev. It uses the same idiom the upload job's stage-prefix arm already does. The `Check configuration` preflight and the debug-pipeline-status variable dump are part of the same change, not a follow-up: the preflight predicts whether the trigger job will run, so leaving it unaware of the new variables would make it report an outcome the pipeline does not produce. Adapted from nc-server rather than copied: ncw's VALID_BRANCH_PATTERN is already per-major aware and is kept over nc-server's looser variant, and the ncw-only `Assert branch major matches version.php` guard stays the thing that stops a lane building one major and deploying under the name of another. Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 113 ++++++++++++++++++++------- 1 file changed, 86 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index dbc610e06c6e0..1b720d86c2d89 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -186,6 +186,8 @@ jobs: echo "### 🔧 Remote Trigger Configuration" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**DISABLE_REMOTE_TRIGGER value:** \`${{ vars.DISABLE_REMOTE_TRIGGER }}\`" >> $GITHUB_STEP_SUMMARY + echo "**REMOTE_TRIGGER_NC_VERSION:** \`${{ vars.REMOTE_TRIGGER_NC_VERSION }}\`" >> $GITHUB_STEP_SUMMARY + echo "**REMOTE_TRIGGER_RC_BRANCH:** \`${{ vars.REMOTE_TRIGGER_RC_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY echo "**Event type:** \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY echo "**Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -193,9 +195,11 @@ jobs: echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "🔧 Remote Trigger Configuration" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" - echo "DISABLE_REMOTE_TRIGGER = '${{ vars.DISABLE_REMOTE_TRIGGER }}'" - echo "Event type = '${{ github.event_name }}'" - echo "Branch = '${{ github.ref_name }}'" + echo "DISABLE_REMOTE_TRIGGER = '${{ vars.DISABLE_REMOTE_TRIGGER }}'" + echo "REMOTE_TRIGGER_NC_VERSION = '${{ vars.REMOTE_TRIGGER_NC_VERSION }}'" + echo "REMOTE_TRIGGER_RC_BRANCH = '${{ vars.REMOTE_TRIGGER_RC_BRANCH }}'" + echo "Event type = '${{ github.event_name }}'" + echo "Branch = '${{ github.ref_name }}'" echo "" if [ "${{ vars.DISABLE_REMOTE_TRIGGER }}" == "true" ]; then @@ -230,6 +234,7 @@ jobs: # Check if branch matches expected patterns: ionos-dev[-v], ionos-stable[-v], rc/* or */dev/* VALID_BRANCH_PATTERN='^(ionos-dev|ionos-stable)(-v[0-9]+)?$|^rc/.*$|^[^/]+/dev/.*$' VALID_BRANCH_DESC="'ionos-dev[-v]', 'ionos-stable[-v]', 'rc/*' or '*/dev/*'" + USER_DEV_PATTERN='^[^/]+/dev/.*$' if [[ ! "${{ github.ref_name }}" =~ $VALID_BRANCH_PATTERN ]]; then echo "- ❌ Branch must be ${VALID_BRANCH_DESC} (current: \`${{ github.ref_name }}\`)" >> $GITHUB_STEP_SUMMARY echo " ❌ Branch is '${{ github.ref_name }}' (must be ${VALID_BRANCH_DESC})" @@ -239,6 +244,43 @@ jobs: echo " ✅ Branch is '${{ github.ref_name }}'" fi + # This block is the shell twin of the version gate in the + # 'trigger-remote-dev-workflow' if: condition. The two are one rule written + # twice — an edit to either must be mirrored in the other, or this summary + # reports an outcome the pipeline does not produce. + if [[ "${{ github.ref_name }}" =~ $USER_DEV_PATTERN ]]; then + echo "- ✅ Version gate: '*/dev/*' branches are not version-gated" >> $GITHUB_STEP_SUMMARY + echo " ✅ Version gate: '*/dev/*' branches are not version-gated" + else + # Bare ionos-dev/ionos-stable are always exempt; the ionos-(dev|stable)-v* branches + # must end with REMOTE_TRIGGER_NC_VERSION; all other branches reaching here (rc/*) + # must match REMOTE_TRIGGER_RC_BRANCH exactly — the suffix check must NOT apply to + # rc/* or an RC branch could pass by coincidence of name. + NC_VERSION="${{ vars.REMOTE_TRIGGER_NC_VERSION }}" + RC_BRANCH="${{ vars.REMOTE_TRIGGER_RC_BRANCH }}" + BRANCH="${{ github.ref_name }}" + + if [ "$BRANCH" == "ionos-dev" ] || [ "$BRANCH" == "ionos-stable" ]; then + echo "- ✅ Version gate: '$BRANCH' is always exempt" >> $GITHUB_STEP_SUMMARY + echo " ✅ Version gate: '$BRANCH' is always exempt" + elif [ -z "$NC_VERSION" ]; then + echo "- ✅ Version gate: not set — all versions allowed" >> $GITHUB_STEP_SUMMARY + echo " ✅ Version gate: not set — all versions allowed" + elif [[ "$BRANCH" == ionos-dev-v* || "$BRANCH" == ionos-stable-v* ]] && [[ "$BRANCH" == *"$NC_VERSION" ]]; then + echo "- ✅ Version gate: branch \`$BRANCH\` matches \`$NC_VERSION\`" >> $GITHUB_STEP_SUMMARY + echo " ✅ Version gate: branch '$BRANCH' matches '$NC_VERSION'" + elif [ -n "$RC_BRANCH" ] && [ "$BRANCH" == "$RC_BRANCH" ]; then + echo "- ✅ Version gate: branch \`$BRANCH\` matches RC whitelist \`$RC_BRANCH\`" >> $GITHUB_STEP_SUMMARY + echo " ✅ Version gate: branch '$BRANCH' matches RC whitelist '$RC_BRANCH'" + else + echo "- ❌ Version gate: branch \`$BRANCH\` does not match version \`$NC_VERSION\` or RC branch \`$RC_BRANCH\`" >> $GITHUB_STEP_SUMMARY + echo "- â„šī¸ Build & image push will still run — only QA deployment is skipped" >> $GITHUB_STEP_SUMMARY + echo " ❌ Version gate: branch '$BRANCH' does not match version '$NC_VERSION' or RC branch '$RC_BRANCH'" + echo " â„šī¸ Build & image push will still run — only QA deployment is skipped" + WILL_TRIGGER=false + fi + fi + echo "- â„šī¸ All dependent jobs must succeed (checked at job runtime)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -1046,30 +1088,46 @@ jobs: name: Trigger remote workflow needs: [build-artifact, upload-to-artifactory] - # Trigger remote build on "ionos-dev|ionos-stable|rc/*|*/dev/*" branch *push* defined in the on:push:branches + # Trigger remote build on "ionos-dev|ionos-stable|ionos-*-v*|rc/*|*/dev/*" branch *push* + # defined in the on:push:branches # Can be disabled via repository variable 'DISABLE_REMOTE_TRIGGER' (set to 'true' to disable) # Configure at: https://github.com/IONOS-Productivity/ncw-server/settings/variables/actions # - # NOTE (NSW-944): the per-major lanes — 'ionos-dev-v*' AND 'ionos-stable-v*' — are deliberately - # absent from the condition below while they DO appear in on:push:branches and in the - # upload-to-artifactory condition. That combination is the Nextcloud-major freeze: a per-major - # integration branch produces build artifacts you can pull with check_release.sh, but can never - # trigger a remote deploy. + # NOTE (NSW-944): the Nextcloud-major freeze lives in repository *variables*, not in this + # condition. The per-major lanes are admitted below and then gated on + # REMOTE_TRIGGER_NC_VERSION, so moving the deployed major is a variable edit rather than a + # workflow edit. Until this change the freeze was hard-coded here as exact-equality arms. # - # The arms below are exact equality on purpose. 'ionos-dev' does not match 'ionos-dev-v32', so - # the freeze holds for every future major without further edits — do not "tidy" these into - # startsWith(), which would silently start deploying the next major. + # Version-gate: only trigger QA deployment for the whitelisted NC major version. + # Set repository variable 'REMOTE_TRIGGER_NC_VERSION' to the deployed version (e.g. "v32"). + # Applies only to 'ionos-dev-v*'/'ionos-stable-v*' branches (checked by suffix); lanes for + # other majors still build & push images but will NOT trigger the remote QA workflow. + # Leave unset (empty) to allow all versions. + # The bare 'ionos-dev'/'ionos-stable' lanes are always exempt from this gate. + # RC-gate: rc/* branches are NOT covered by the NC-version suffix check above — the current + # train 'rc/ncw-7' encodes no major at all, and applying a suffix check to rc/* would let a + # branch pass by coincidence of name. Set 'REMOTE_TRIGGER_RC_BRANCH' to the exact RC branch + # that should trigger QA (e.g. "rc/ncw-7", or "rc/ncw-v32-1" in the current naming era). + # Only one RC at a time; when promoting a new RC, update the variable to the new branch name. + # */dev/* branches bypass the version gate entirely. # - # Widen this only at a cutover, when the deployed major is meant to change. Doing so also - # requires widening the BUILD_TYPE stable arm further down, which is exact-match for the same - # reason; otherwise a newly-admitted ionos-stable-v would deploy as BUILD_TYPE=dev. + # Widening the gate at a cutover also requires widening the BUILD_TYPE stable arm further + # down; otherwise a newly-admitted ionos-stable-v would deploy as BUILD_TYPE=dev. if: | always() && github.event_name == 'push' && - (github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' || startsWith(github.ref_name, 'rc/') || contains(github.ref_name, '/dev/')) && + (github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' || + startsWith(github.ref_name, 'ionos-dev-v') || startsWith(github.ref_name, 'ionos-stable-v') || + startsWith(github.ref_name, 'rc/') || contains(github.ref_name, '/dev/')) && needs.build-artifact.result == 'success' && needs.upload-to-artifactory.result == 'success' && - vars.DISABLE_REMOTE_TRIGGER != 'true' + vars.DISABLE_REMOTE_TRIGGER != 'true' && + (contains(github.ref_name, '/dev/') || + github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' || + vars.REMOTE_TRIGGER_NC_VERSION == '' || + ((startsWith(github.ref_name, 'ionos-dev-v') || startsWith(github.ref_name, 'ionos-stable-v')) && + endsWith(github.ref_name, vars.REMOTE_TRIGGER_NC_VERSION)) || + github.ref_name == vars.REMOTE_TRIGGER_RC_BRANCH) steps: - name: Check prerequisites run: | @@ -1140,14 +1198,14 @@ jobs: # | ref_name | GITLAB_REF | BUILD_TYPE | # |------------------|--------------|-------------| # | ionos-dev | main | dev | + # | ionos-dev-v* | main | dev | # | ionos-stable | main | stable | + # | ionos-stable-v* | main | stable | # | rc/* | main | rc | # | */dev/* | main | dev-* | # - # The per-major lanes — ionos-dev-v* and ionos-stable-v* — have no rows because they - # never reach this job: the if: condition above matches the unsuffixed names by exact - # equality. That is the freeze. Do not add speculative arms for them here; an - # unreachable arm reads as support that does not exist. + # BUILD_TYPE only says which lane a build came from — which major it carries is + # NC_MAJOR below, so the per-major lanes share their unsuffixed lane's BUILD_TYPE. # # GITLAB_REF stays 'main' for every lane and BUILD_TYPE keeps its existing values — # both are consumed on the GitLab side, so changing either needs a coordinated change @@ -1155,12 +1213,11 @@ jobs: BUILD_TYPE="dev" - # Override build type for the stable branch. Exact match, deliberately unlike the - # upload job's stage-prefix arm, which does accept ionos-stable-v: that job's gate - # admits the per-major lanes and this one does not. Widening the gate at a cutover - # must widen this arm in the same change, or a suffixed stable lane would deploy as - # BUILD_TYPE=dev. - if [ "${{ github.ref_name }}" == "ionos-stable" ]; then + # Override build type for the stable branches. Matches the suffixed form too, the same + # way the upload job's stage-prefix arm does: both jobs now admit the per-major lanes, + # and without this arm a suffixed stable lane would deploy as BUILD_TYPE=dev. The dev + # lanes need no arm — ionos-dev and ionos-dev-v both fall through to the default. + if [[ "${{ github.ref_name }}" == "ionos-stable" || "${{ github.ref_name }}" == ionos-stable-v[0-9]* ]]; then BUILD_TYPE="stable" # Override build type for rc/* branches elif [[ "${{ github.ref_name }}" =~ ^rc/ ]]; then @@ -1469,6 +1526,8 @@ jobs: echo "" echo "### Repository Variables" echo "DISABLE_REMOTE_TRIGGER: ${{ vars.DISABLE_REMOTE_TRIGGER || 'not set' }}" + echo "REMOTE_TRIGGER_NC_VERSION: ${{ vars.REMOTE_TRIGGER_NC_VERSION || 'not set' }}" + echo "REMOTE_TRIGGER_RC_BRANCH: ${{ vars.REMOTE_TRIGGER_RC_BRANCH || 'not set' }}" echo "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" From a718a2ec940db522f1291dee284a63c0c75d3e96 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Sun, 30 Aug 2026 20:47:08 +0200 Subject: [PATCH 2/4] IONOS(ci): gate */dev/* GitLab trigger behind ENABLE_REMOTE_TRIGGER_USER_DEV MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every push to a `*/dev/*` user dev branch triggers a QA deployment today. That is unconditional: the only kill switch is DISABLE_REMOTE_TRIGGER, which is repo-wide and takes the protected lanes down with it. Port nc-server's opt-in (IONOS-Productivity/nc-server e3e638e1201): the trigger job now additionally requires ENABLE_REMOTE_TRIGGER_USER_DEV == 'true' for that branch class. `*/dev/*` branches keep building and keep uploading to Artifactory — only the GitLab QA trigger becomes opt-in. This is the one intentional behaviour change in this series. Set the repository variable to 'true' to restore the previous behaviour: https://github.com/IONOS-Productivity/ncw-server/settings/variables/actions The opt-in sits in the branch-admission clause, not in the version gate, so `*/dev/*` still bypasses REMOTE_TRIGGER_NC_VERSION once opted in — a user dev branch carries no major in its name and there is nothing to compare against. Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 31 ++++++++++++++++++++++++---- 1 file changed, 27 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 1b720d86c2d89..89a4fc1961d99 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -186,6 +186,10 @@ jobs: echo "### 🔧 Remote Trigger Configuration" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**DISABLE_REMOTE_TRIGGER value:** \`${{ vars.DISABLE_REMOTE_TRIGGER }}\`" >> $GITHUB_STEP_SUMMARY + echo "**ENABLE_REMOTE_TRIGGER_USER_DEV value:** \`${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}\`" >> $GITHUB_STEP_SUMMARY + if [ "${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}" != "true" ]; then + echo " - 💡 To enable the GitLab trigger for \`*/dev/*\` branches, set repository variable \`ENABLE_REMOTE_TRIGGER_USER_DEV\` to \`true\` at [Settings → Variables → Actions](https://github.com/${{ github.repository }}/settings/variables/actions)." >> $GITHUB_STEP_SUMMARY + fi echo "**REMOTE_TRIGGER_NC_VERSION:** \`${{ vars.REMOTE_TRIGGER_NC_VERSION }}\`" >> $GITHUB_STEP_SUMMARY echo "**REMOTE_TRIGGER_RC_BRANCH:** \`${{ vars.REMOTE_TRIGGER_RC_BRANCH }}\`" >> $GITHUB_STEP_SUMMARY echo "**Event type:** \`${{ github.event_name }}\`" >> $GITHUB_STEP_SUMMARY @@ -196,6 +200,12 @@ jobs: echo "🔧 Remote Trigger Configuration" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" echo "DISABLE_REMOTE_TRIGGER = '${{ vars.DISABLE_REMOTE_TRIGGER }}'" + echo "ENABLE_REMOTE_TRIGGER_USER_DEV = '${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}'" + if [ "${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}" != "true" ]; then + echo " 💡 To enable the GitLab trigger for '*/dev/*' branches," + echo " set repository variable ENABLE_REMOTE_TRIGGER_USER_DEV to 'true' at:" + echo " https://github.com/${{ github.repository }}/settings/variables/actions" + fi echo "REMOTE_TRIGGER_NC_VERSION = '${{ vars.REMOTE_TRIGGER_NC_VERSION }}'" echo "REMOTE_TRIGGER_RC_BRANCH = '${{ vars.REMOTE_TRIGGER_RC_BRANCH }}'" echo "Event type = '${{ github.event_name }}'" @@ -249,8 +259,17 @@ jobs: # twice — an edit to either must be mirrored in the other, or this summary # reports an outcome the pipeline does not produce. if [[ "${{ github.ref_name }}" =~ $USER_DEV_PATTERN ]]; then - echo "- ✅ Version gate: '*/dev/*' branches are not version-gated" >> $GITHUB_STEP_SUMMARY - echo " ✅ Version gate: '*/dev/*' branches are not version-gated" + # Not version-gated; governed solely by the ENABLE_REMOTE_TRIGGER_USER_DEV opt-in. + if [ "${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}" == "true" ]; then + echo "- ✅ User-dev branch opt-in (\`ENABLE_REMOTE_TRIGGER_USER_DEV='true'\`)" >> $GITHUB_STEP_SUMMARY + echo " ✅ ENABLE_REMOTE_TRIGGER_USER_DEV='true' — '*/dev/*' trigger is opted in" + else + echo "- ❌ User-dev branch requires \`ENABLE_REMOTE_TRIGGER_USER_DEV='true'\` (current: \`${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}\`)" >> $GITHUB_STEP_SUMMARY + echo "- â„šī¸ Build & image push will still run — only QA deployment is skipped" >> $GITHUB_STEP_SUMMARY + echo " ❌ '*/dev/*' branch requires ENABLE_REMOTE_TRIGGER_USER_DEV='true' (current: '${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}')" + echo " â„šī¸ Build & image push will still run — only QA deployment is skipped" + WILL_TRIGGER=false + fi else # Bare ionos-dev/ionos-stable are always exempt; the ionos-(dev|stable)-v* branches # must end with REMOTE_TRIGGER_NC_VERSION; all other branches reaching here (rc/*) @@ -1109,7 +1128,9 @@ jobs: # branch pass by coincidence of name. Set 'REMOTE_TRIGGER_RC_BRANCH' to the exact RC branch # that should trigger QA (e.g. "rc/ncw-7", or "rc/ncw-v32-1" in the current naming era). # Only one RC at a time; when promoting a new RC, update the variable to the new branch name. - # */dev/* branches bypass the version gate entirely. + # */dev/* branches bypass the version gate entirely and are controlled solely by repository + # variable 'ENABLE_REMOTE_TRIGGER_USER_DEV' (default off — set to 'true' to let user dev + # branches deploy to QA). They still build and still upload to Artifactory when off. # # Widening the gate at a cutover also requires widening the BUILD_TYPE stable arm further # down; otherwise a newly-admitted ionos-stable-v would deploy as BUILD_TYPE=dev. @@ -1118,7 +1139,8 @@ jobs: github.event_name == 'push' && (github.ref_name == 'ionos-dev' || github.ref_name == 'ionos-stable' || startsWith(github.ref_name, 'ionos-dev-v') || startsWith(github.ref_name, 'ionos-stable-v') || - startsWith(github.ref_name, 'rc/') || contains(github.ref_name, '/dev/')) && + startsWith(github.ref_name, 'rc/') || + (contains(github.ref_name, '/dev/') && vars.ENABLE_REMOTE_TRIGGER_USER_DEV == 'true')) && needs.build-artifact.result == 'success' && needs.upload-to-artifactory.result == 'success' && vars.DISABLE_REMOTE_TRIGGER != 'true' && @@ -1526,6 +1548,7 @@ jobs: echo "" echo "### Repository Variables" echo "DISABLE_REMOTE_TRIGGER: ${{ vars.DISABLE_REMOTE_TRIGGER || 'not set' }}" + echo "ENABLE_REMOTE_TRIGGER_USER_DEV: ${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV || 'not set' }}" echo "REMOTE_TRIGGER_NC_VERSION: ${{ vars.REMOTE_TRIGGER_NC_VERSION || 'not set' }}" echo "REMOTE_TRIGGER_RC_BRANCH: ${{ vars.REMOTE_TRIGGER_RC_BRANCH || 'not set' }}" echo "" From 44538d5b2aeb8c06cdc291687a94e92a1b451ed1 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Sun, 30 Aug 2026 20:47:33 +0200 Subject: [PATCH 3/4] IONOS(ci): forward the source ref name to the remote trigger BUILD_TYPE says which lane a build came from ('dev', 'stable', 'rc', 'dev-') and NC_MAJOR says which Nextcloud major it carries, but neither names the branch. A downstream release report could not tell 'rc/ncw-6' from 'rc/ncw-7', nor link back to the source branch. Forward github.ref_name and github.repository as GITHUB_REF_NAME and GITHUB_REPOSITORY, as nc-server does (IONOS-Productivity/nc-server 3b676abd6da, HDNEXT-1373). Purely additive: two new trigger variables. Existing GitLab-side consumers of GITHUB_SHA, NC_VERSION, NC_MAJOR, BUILD_ID, BUILD_TYPE and SOURCE_BUILD_URL are untouched, so this needs no coordinated change there. Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 89a4fc1961d99..38a9d8d104734 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -1228,6 +1228,8 @@ jobs: # # BUILD_TYPE only says which lane a build came from — which major it carries is # NC_MAJOR below, so the per-major lanes share their unsuffixed lane's BUILD_TYPE. + # The ref name is forwarded verbatim as well, so the downstream release report can + # name the exact RC or branch and link back to it. # # GITLAB_REF stays 'main' for every lane and BUILD_TYPE keeps its existing values — # both are consumed on the GitLab side, so changing either needs a coordinated change @@ -1286,6 +1288,8 @@ jobs: --form "variables[NC_MAJOR]=${NC_MAJOR}" \ --form "variables[BUILD_ID]=${{ github.run_id }}" \ --form "variables[BUILD_TYPE]=${BUILD_TYPE}" \ + --form "variables[GITHUB_REF_NAME]=${{ github.ref_name }}" \ + --form "variables[GITHUB_REPOSITORY]=${{ github.repository }}" \ --form "variables[SOURCE_BUILD_URL]=${SOURCE_BUILD_URL}" \ "${{ secrets.GITLAB_TRIGGER_URL }}"; then TRIGGER_SUCCESS=true From 82a1a66bfb5efe8c223d2e084b3d606bcfd4f935 Mon Sep 17 00:00:00 2001 From: "Misha M.-Kupriyanov" Date: Mon, 31 Aug 2026 13:37:51 +0200 Subject: [PATCH 4/4] IONOS(ci): say why the remote trigger was skipped MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A skipped 'trigger-remote-dev-workflow' reported nothing but 'skipped' in the Debug Pipeline Status summary, which is the summary people actually read. The prepare-matrix preflight did explain it, but in a different job's log — so diagnosing why run 33374512576 produced no QA deployment meant downloading the run's log archive to find the one line that said DISABLE_REMOTE_TRIGGER='true'. Publish the preflight's verdict instead of re-deriving it. 'Check configuration' gains an id and records a TRIGGER_REASON next to the WILL_TRIGGER it already computes; prepare-matrix exposes both as job outputs; debug-pipeline-status — which already depends on prepare-matrix — quotes the reason next to the skipped job, in the log and in the step summary. Deliberately a consumer, not a third implementation. The gate is already encoded twice, in the trigger job's if: and in this preflight, and those two are kept in agreement by hand; a third copy in the debug job would be one more thing to forget. The debug job contributes only what the preflight cannot know, because it runs before the build does: whether a dependency failed. Precedence is dependency failure first, then the preflight's reason, then an empty reason degrading to the bare 'skipped' of today, which is what happens when prepare-matrix itself failed. Reasons are recorded first-failure-wins via a set_reason helper rather than by assignment. The checks are not mutually exclusive — a branch that is not a build lane also falls through to the version gate — so plain assignment reported 'master' as a version mismatch instead of "not a build lane". No change to the gate: both verification harnesses still pass with identical verdicts on all 19 cases, and the preflight still agrees with the if: expression on every one. The harness now also asserts the published output is 'true'/'false' in step with the verdict, and that a reason is present exactly when the trigger is skipped. Signed-off-by: Misha M.-Kupriyanov --- .github/workflows/build-artifact.yml | 62 ++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index 38a9d8d104734..052d5d6cfd1b6 100644 --- a/.github/workflows/build-artifact.yml +++ b/.github/workflows/build-artifact.yml @@ -104,6 +104,9 @@ jobs: has_apps_to_build: ${{ steps.detect.outputs.has_apps_to_build }} has_apps_to_restore: ${{ steps.detect.outputs.has_apps_to_restore }} effective_cache_version: ${{ steps.compute_cache_version.outputs.effective_cache_version }} + # Consumed by debug-pipeline-status to explain a skipped trigger-remote-dev-workflow. + remote_trigger_expected: ${{ steps.check_config.outputs.remote_trigger_expected }} + remote_trigger_reason: ${{ steps.check_config.outputs.remote_trigger_reason }} steps: - name: Compute effective cache version id: compute_cache_version @@ -180,7 +183,13 @@ jobs: - name: Install dependencies run: sudo apt-get update && sudo apt-get install -y make jq + # Besides printing the configuration, this step publishes its verdict as job outputs + # (remote_trigger_expected / remote_trigger_reason) so debug-pipeline-status can say *why* + # the trigger job was skipped instead of reporting a bare 'skipped'. That job consumes the + # verdict rather than re-deriving it — the gate is already encoded twice (here and in the + # trigger job's if:), and a third copy would be a maintenance trap. - name: Check configuration + id: check_config run: | echo "" echo "### 🔧 Remote Trigger Configuration" >> $GITHUB_STEP_SUMMARY @@ -212,7 +221,22 @@ jobs: echo "Branch = '${{ github.ref_name }}'" echo "" + # TRIGGER_REASON names the gate that blocked, and is published below for + # debug-pipeline-status to quote. It is only meaningful when WILL_TRIGGER=false. + WILL_TRIGGER=false + TRIGGER_REASON="" + + # First failing gate wins. The checks below are not mutually exclusive — an invalid + # branch also falls through to the version gate — so a later one must not overwrite the + # reason an earlier one recorded, or e.g. 'master' would be reported as a version + # mismatch rather than as "not a build lane". Each check still prints its own finding. + set_reason() { + if [ -z "$TRIGGER_REASON" ]; then TRIGGER_REASON="$1"; fi + return 0 + } + if [ "${{ vars.DISABLE_REMOTE_TRIGGER }}" == "true" ]; then + set_reason "DISABLE_REMOTE_TRIGGER='true' — remote trigger force-disabled repo-wide" echo "âš ī¸ Remote trigger is DISABLED" echo " The 'trigger-remote-dev-workflow' job will be SKIPPED" echo "**Status:** âš ī¸ Remote trigger is **DISABLED**" >> $GITHUB_STEP_SUMMARY @@ -235,6 +259,7 @@ jobs: if [ "${{ github.event_name }}" != "push" ]; then echo "- ❌ Event must be 'push' (current: \`${{ github.event_name }}\`)" >> $GITHUB_STEP_SUMMARY echo " ❌ Event type is '${{ github.event_name }}' (must be 'push')" + set_reason "event is '${{ github.event_name }}', the trigger only runs on 'push'" WILL_TRIGGER=false else echo "- ✅ Event is 'push'" >> $GITHUB_STEP_SUMMARY @@ -248,6 +273,7 @@ jobs: if [[ ! "${{ github.ref_name }}" =~ $VALID_BRANCH_PATTERN ]]; then echo "- ❌ Branch must be ${VALID_BRANCH_DESC} (current: \`${{ github.ref_name }}\`)" >> $GITHUB_STEP_SUMMARY echo " ❌ Branch is '${{ github.ref_name }}' (must be ${VALID_BRANCH_DESC})" + set_reason "branch '${{ github.ref_name }}' is not a build lane (expected ${VALID_BRANCH_DESC})" WILL_TRIGGER=false else echo "- ✅ Branch is '\`${{ github.ref_name }}\`'" >> $GITHUB_STEP_SUMMARY @@ -268,6 +294,7 @@ jobs: echo "- â„šī¸ Build & image push will still run — only QA deployment is skipped" >> $GITHUB_STEP_SUMMARY echo " ❌ '*/dev/*' branch requires ENABLE_REMOTE_TRIGGER_USER_DEV='true' (current: '${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}')" echo " â„šī¸ Build & image push will still run — only QA deployment is skipped" + set_reason "'*/dev/*' branch needs ENABLE_REMOTE_TRIGGER_USER_DEV='true' (currently '${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}')" WILL_TRIGGER=false fi else @@ -296,6 +323,7 @@ jobs: echo "- â„šī¸ Build & image push will still run — only QA deployment is skipped" >> $GITHUB_STEP_SUMMARY echo " ❌ Version gate: branch '$BRANCH' does not match version '$NC_VERSION' or RC branch '$RC_BRANCH'" echo " â„šī¸ Build & image push will still run — only QA deployment is skipped" + set_reason "version gate: '$BRANCH' matches neither REMOTE_TRIGGER_NC_VERSION='$NC_VERSION' nor REMOTE_TRIGGER_RC_BRANCH='$RC_BRANCH'" WILL_TRIGGER=false fi fi @@ -315,6 +343,14 @@ jobs: fi echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━" + # Publish the verdict for debug-pipeline-status. Only the branch/event/variable gates are + # visible from here — whether the dependent jobs succeed is not known until much later, + # so the consumer checks those itself and falls back to this reason. + { + echo "remote_trigger_expected=${WILL_TRIGGER}" + echo "remote_trigger_reason=${TRIGGER_REASON}" + } >> "$GITHUB_OUTPUT" + - name: List caches before restore run: gh cache list env: @@ -1412,6 +1448,26 @@ jobs: analyze_job "upload-to-artifactory" "${{ needs.upload-to-artifactory.result }}" analyze_job "nextcloud-workspace-artifact-to-ghcr_io" "${{ needs.nextcloud-workspace-artifact-to-ghcr_io.result }}" analyze_job "trigger-remote-dev-workflow" "${{ needs.trigger-remote-dev-workflow.result }}" + + # Explain a skipped remote trigger. Two sources, in precedence order: + # 1. a dependency did not succeed — prepare-matrix ran too early to know this + # 2. otherwise, the branch/event/variable verdict prepare-matrix published + # Falls back to empty (and so to a bare 'skipped', as before) when prepare-matrix itself + # did not complete and published no reason. + TRIGGER_SKIP_REASON="" + if [ "${{ needs.trigger-remote-dev-workflow.result }}" == "skipped" ]; then + if [ "${{ needs.build-artifact.result }}" != "success" ]; then + TRIGGER_SKIP_REASON="build-artifact did not succeed (${{ needs.build-artifact.result }})" + elif [ "${{ needs.upload-to-artifactory.result }}" != "success" ]; then + TRIGGER_SKIP_REASON="upload-to-artifactory did not succeed (${{ needs.upload-to-artifactory.result }})" + else + TRIGGER_SKIP_REASON="${{ needs.prepare-matrix.outputs.remote_trigger_reason }}" + fi + fi + + if [ -n "$TRIGGER_SKIP_REASON" ]; then + echo " â†ŗ reason: $TRIGGER_SKIP_REASON" + fi echo "" # Overall pipeline status @@ -1501,6 +1557,12 @@ jobs: echo "| nextcloud-workspace-artifact-to-ghcr_io | ${{ needs.nextcloud-workspace-artifact-to-ghcr_io.result == 'success' && '✅' || needs.nextcloud-workspace-artifact-to-ghcr_io.result == 'failure' && '❌' || needs.nextcloud-workspace-artifact-to-ghcr_io.result == 'skipped' && 'â­ī¸' || '❓' }} ${{ needs.nextcloud-workspace-artifact-to-ghcr_io.result }} |" echo "| trigger-remote-dev-workflow | ${{ needs.trigger-remote-dev-workflow.result == 'success' && '✅' || needs.trigger-remote-dev-workflow.result == 'failure' && '❌' || needs.trigger-remote-dev-workflow.result == 'skipped' && 'â­ī¸' || '❓' }} ${{ needs.trigger-remote-dev-workflow.result }} |" echo "" + if [ -n "$TRIGGER_SKIP_REASON" ]; then + echo "> â­ī¸ **No remote QA deployment was triggered:** $TRIGGER_SKIP_REASON" + echo ">" + echo "> Configure at [Settings → Variables → Actions](${{ github.server_url }}/${{ github.repository }}/settings/variables/actions)." + echo "" + fi if [ -n "$FAILED_JOBS" ]; then echo "## ❌ Pipeline Status: FAILED"