diff --git a/.github/workflows/build-artifact.yml b/.github/workflows/build-artifact.yml index dbc610e06c6e0..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,12 +183,24 @@ 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 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 echo "**Branch:** \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -193,12 +208,35 @@ 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 "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 }}'" + 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 @@ -221,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 @@ -230,15 +269,65 @@ 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})" + 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 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 + # 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" + set_reason "'*/dev/*' branch needs ENABLE_REMOTE_TRIGGER_USER_DEV='true' (currently '${{ vars.ENABLE_REMOTE_TRIGGER_USER_DEV }}')" + 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/*) + # 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" + 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 + echo "- â„šī¸ All dependent jobs must succeed (checked at job runtime)" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY @@ -254,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: @@ -1046,30 +1143,49 @@ 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 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. # - # 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/') && vars.ENABLE_REMOTE_TRIGGER_USER_DEV == 'true')) && 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 +1256,16 @@ 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. + # 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 @@ -1155,12 +1273,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 @@ -1207,6 +1324,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 @@ -1329,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 @@ -1418,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" @@ -1469,6 +1614,9 @@ 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 "" echo "━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━"