diff --git a/.github/WORKFLOWS.md b/.github/WORKFLOWS.md index 4846b78b41e..da9a81b3406 100644 --- a/.github/WORKFLOWS.md +++ b/.github/WORKFLOWS.md @@ -805,14 +805,20 @@ Embargoed security work happens in `n8n-io/n8n-private`. `sec-sync-public-to-pri runs hourly there (and on `workflow_dispatch` with `force` for conflict recovery), mirroring public `master` and `1.x` into private with `reset --hard` + `--force-with-lease` — skipping a branch when private is ahead, ignoring `chore: Bundle` -commits when judging "ahead". Fixes are never committed to private `master`/`1.x` -directly: `ci-restrict-private-merges.yml` requires PRs into them to come from the -long-lived integration branches `bundle/2.x` and `bundle/1.x` (a `bundle/2.x` merge is +commits when judging "ahead". A skipped branch, or a hard failure, is reported to +`#alerts-build`: a non-`chore: Bundle` commit on private `master`/`1.x` leaves the mirror +stuck every hour until it is removed or a `force` dispatch overwrites it. Fixes are never +committed to private `master`/`1.x` directly: `ci-restrict-private-merges.yml` requires +PRs into them to come from the long-lived integration +branches `bundle/2.x` and `bundle/1.x` (a `bundle/2.x` merge is backported to `bundle/1.x` by `util-backport-bundle.yml`). Once a bundle branch is merged into private `master`/`1.x` as a `chore: Bundle/*` PR, `sec-publish-fix.yml` / `sec-publish-fix-1x.yml` cherry-pick that commit onto a fresh branch in the public repo and open the PR there. That PR **must stay a single-parent squash** — the publish step is a bare -`git cherry-pick` of `HEAD`, which aborts on a merge commit. +`git cherry-pick` of `HEAD`, which aborts on a merge commit. A `chore: Bundle/*` PR whose +*Required Checks* go red holds back every fix batched into it, so `ci-pull-requests.yml` +posts to `#alerts-build` when that gate fails on a PR opened *from* `bundle/2.x` or +`bundle/1.x` (link only, no PR title, since the branch is embargoed). `sec-sync-bundle-branches.yml` keeps those branches current, daily plus whenever a PR is merged into one (and on `workflow_dispatch`). It **merges the base into** the bundle branch @@ -886,7 +892,7 @@ If notify is a step inside an existing checked-out job, skip the `checkout` and | `QBOT_SLACK_TOKEN` | QBot | Default — engineering / build / security | | `RELEASE_HELPER_SLACK_TOKEN` | Release Helper | `#releases` (C036AELNMV0) | -Adding a new channel requires inviting the bot first; the first run otherwise fails loudly with `not_in_channel`. Private-repo workflows (`sec-publish-fix*.yml`) need `QBOT_SLACK_TOKEN` set in `n8n-io/n8n-private`; the scripts themselves are mirrored by `sec-sync-public-to-private.yml`. +Adding a new channel requires inviting the bot first; the first run otherwise fails loudly with `not_in_channel`. Private-repo workflows (`sec-publish-fix*.yml`, `sec-sync-public-to-private.yml`, and the bundle-PR alert in `ci-pull-requests.yml`) need `QBOT_SLACK_TOKEN` set in `n8n-io/n8n-private`; the scripts themselves are mirrored by `sec-sync-public-to-private.yml`. --- diff --git a/.github/workflows/ci-pull-requests.yml b/.github/workflows/ci-pull-requests.yml index 53902c29baa..5f3bf6d290d 100644 --- a/.github/workflows/ci-pull-requests.yml +++ b/.github/workflows/ci-pull-requests.yml @@ -412,6 +412,33 @@ jobs: mode: validate job-results: ${{ toJSON(needs) }} + # A red `chore: Bundle/*` PR blocks publishing every fix batched into it, so the + # failure is announced instead of waiting to be noticed. Only fires in n8n-private, + # where bundle branches exist. + notify-bundle-pr-failure: + name: Notify Slack on bundle PR failure + needs: required-checks + if: >- + always() && + needs.required-checks.result == 'failure' && + (github.head_ref == 'bundle/2.x' || github.head_ref == 'bundle/1.x') + runs-on: ubuntu-slim + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + sparse-checkout: .github/scripts/slack + sparse-checkout-cone-mode: false + - name: Notify Slack + env: + SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }} + HEAD_REF: ${{ github.head_ref }} + PR_URL: ${{ github.event.pull_request.html_url }} + run: | + node .github/scripts/slack/notify.mjs \ + --channel '#alerts-build' \ + --text "<${PR_URL}|Required Checks failed on the ${HEAD_REF} PR> - the batched fixes stay unpublished until it is green." + # Posts a QA metrics comparison comment on the PR. # Runs after all checks so any job can emit metrics before this reports. post-qa-metrics-comment: diff --git a/.github/workflows/sec-sync-public-to-private.yml b/.github/workflows/sec-sync-public-to-private.yml index 86124100783..3d2d4abfc45 100644 --- a/.github/workflows/sec-sync-public-to-private.yml +++ b/.github/workflows/sec-sync-public-to-private.yml @@ -6,6 +6,8 @@ # Scheduled runs only sync if private is not ahead of public. # Manual runs always sync (for conflict recovery after failed cherry-pick). # +# A skipped branch or a hard failure is reported to #alerts-build. +# # The bundle/* integration branches are kept current by sec-sync-bundle-branches.yml. name: 'Security: Sync from Public' @@ -44,6 +46,7 @@ jobs: token: ${{ steps.app-token.outputs.token }} - name: Sync master from public + id: sync-master env: EVENT_NAME: ${{ github.event_name }} FORCE: ${{ inputs.force }} @@ -56,9 +59,11 @@ jobs: if [ "$AHEAD_COUNT" -gt 0 ]; then if [ "$EVENT_NAME" = "schedule" ]; then echo "Private is $AHEAD_COUNT commit(s) ahead of public, skipping scheduled sync" + echo "ahead_count=$AHEAD_COUNT" >> "$GITHUB_OUTPUT" exit 0 elif [ "$FORCE" != "true" ]; then echo "Private is $AHEAD_COUNT commit(s) ahead of public, skipping (force not enabled)" + echo "ahead_count=$AHEAD_COUNT" >> "$GITHUB_OUTPUT" exit 0 else echo "Private is $AHEAD_COUNT commit(s) ahead of public, force syncing anyway" @@ -69,6 +74,7 @@ jobs: git push origin master --force-with-lease - name: Sync 1.x from public + id: sync-1x env: EVENT_NAME: ${{ github.event_name }} FORCE: ${{ inputs.force }} @@ -82,9 +88,11 @@ jobs: if [ "$AHEAD_COUNT" -gt 0 ]; then if [ "$EVENT_NAME" = "schedule" ]; then echo "Private 1.x is $AHEAD_COUNT commit(s) ahead of public, skipping scheduled sync" + echo "ahead_count=$AHEAD_COUNT" >> "$GITHUB_OUTPUT" exit 0 elif [ "$FORCE" != "true" ]; then echo "Private 1.x is $AHEAD_COUNT commit(s) ahead of public, skipping (force not enabled)" + echo "ahead_count=$AHEAD_COUNT" >> "$GITHUB_OUTPUT" exit 0 else echo "Private 1.x is $AHEAD_COUNT commit(s) ahead of public, force syncing anyway" @@ -114,3 +122,37 @@ jobs: echo "Missing: ${MISSING[*]} - dispatching Security: Sync Bundle Branches" gh workflow run sec-sync-bundle-branches.yml --ref master + + - name: Notify Slack on failure + if: failure() + env: + SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + continue-on-error: true + run: | + node .github/scripts/slack/notify.mjs \ + --channel '#alerts-build' \ + --text "<${RUN_URL}|Public → private sync failed>" + + - name: Notify Slack on skipped sync + continue-on-error: true + if: ${{ !cancelled() && (steps.sync-master.outputs.ahead_count || steps.sync-1x.outputs.ahead_count) }} + env: + SLACK_TOKEN: ${{ secrets.QBOT_SLACK_TOKEN }} + MASTER_AHEAD: ${{ steps.sync-master.outputs.ahead_count }} + ONE_X_AHEAD: ${{ steps.sync-1x.outputs.ahead_count }} + RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + run: | + DETAILS='' + if [ -n "$MASTER_AHEAD" ]; then + DETAILS="master is $MASTER_AHEAD commit(s) ahead" + fi + if [ -n "$ONE_X_AHEAD" ]; then + [ -z "$DETAILS" ] || DETAILS="$DETAILS, " + DETAILS="${DETAILS}1.x is $ONE_X_AHEAD commit(s) ahead" + fi + + # Non-Bundle commits on private master/1.x keep the sync skipped until they are removed or force-synced + node .github/scripts/slack/notify.mjs \ + --channel '#alerts-build' \ + --text "<${RUN_URL}|Public → private sync skipped>: ${DETAILS} of public. Re-run with force once resolved."