diff --git a/.github/BRANCH_PROTECTION.md b/.github/BRANCH_PROTECTION.md new file mode 100644 index 00000000..657ee307 --- /dev/null +++ b/.github/BRANCH_PROTECTION.md @@ -0,0 +1,50 @@ +# Default branch protection + +The intended repository ruleset is stored in +[`rulesets/protect-default-branch.json`](rulesets/protect-default-branch.json). +It targets the configured default branch and requires pull requests, the strict +`Required CI` check from GitHub Actions, resolved review conversations, +squash-only merges, and linear history. Branch deletion and force pushes are +blocked. Qodana remains advisory and is not part of the required gate. + +The ruleset's `RepositoryRole` actor ID `5` is GitHub's built-in Administrator +role. Its `pull_request` bypass mode permits an administrator to bypass rules +only while merging an existing pull request; it does not permit a direct push. + +## Repository settings + +Keep squash merge enabled and disable merge commits and rebase merge. Enable +auto-merge, the update-branch option, and automatic deletion of merged head +branches. Keep the Actions token read-only and do not allow Actions to approve +pull-request reviews. + +## Activation and verification + +1. Confirm a pull request and a `master` push each produce exactly one + successful `Required CI` check from GitHub Actions integration `15368`. +2. For the initial ruleset POST, override the committed JSON's `enforcement` + value to `disabled`, then compare GitHub's normalized API response with this + file. +3. PUT the exact committed JSON to the inspected ruleset to activate it. +4. Open a canary pull request and confirm that pending or failing CI, unresolved + conversations, and an out-of-date branch block merging; only squash merge is + available. +5. After merging, confirm the head branch is deleted and the post-merge full + E2E, frontend release, and mobile release guards still run. + +If validation exposes a blocker, disable the ruleset rather than deleting it so +its configuration and history remain available. + +## Normal publishing flow + +Create a scoped feature branch, open a pull request to `master`, wait for +`Required CI`, update the branch if `master` advanced, resolve every review +conversation, and squash-merge. For waits expected to exceed 90 seconds, use +the workspace `scripts/ci-watch.sh` helper instead of repeatedly polling GitHub. + +## Break glass + +For an incident, an administrator must still open a pull request. Document the +incident and why the normal gate cannot complete, then use the PR-only bypass +when merging. Monitor all post-merge workflows and open a follow-up pull request +for any validation or remediation deferred during the incident. diff --git a/.github/rulesets/protect-default-branch.json b/.github/rulesets/protect-default-branch.json new file mode 100644 index 00000000..8a3d3482 --- /dev/null +++ b/.github/rulesets/protect-default-branch.json @@ -0,0 +1,53 @@ +{ + "name": "Protect default branch", + "target": "branch", + "enforcement": "active", + "bypass_actors": [ + { + "actor_id": 5, + "actor_type": "RepositoryRole", + "bypass_mode": "pull_request" + } + ], + "conditions": { + "ref_name": { + "include": ["~DEFAULT_BRANCH"], + "exclude": [] + } + }, + "rules": [ + { + "type": "deletion" + }, + { + "type": "non_fast_forward" + }, + { + "type": "required_linear_history" + }, + { + "type": "pull_request", + "parameters": { + "allowed_merge_methods": ["squash"], + "dismiss_stale_reviews_on_push": false, + "require_code_owner_review": false, + "require_last_push_approval": false, + "required_approving_review_count": 0, + "required_review_thread_resolution": true + } + }, + { + "type": "required_status_checks", + "parameters": { + "do_not_enforce_on_create": false, + "required_status_checks": [ + { + "context": "Required CI", + "integration_id": 15368 + } + ], + "strict_required_status_checks_policy": true + } + } + ] +} diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml index 70b929ac..3ec1fc1e 100644 --- a/.github/workflows/code_quality.yml +++ b/.github/workflows/code_quality.yml @@ -2,9 +2,9 @@ name: Qodana Configuration Upload on: push: - branches: [main, dev] + branches: [master, beta, canary, internal] pull_request: - branches: [main] + branches: [master, beta, canary, internal] workflow_dispatch: permissions: @@ -19,7 +19,34 @@ jobs: - name: Checkout repository uses: actions/checkout@v5 + - name: Detect Qodana upload prerequisites + id: qodana-upload-prerequisites + shell: bash + env: + QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }} + run: | + set -euo pipefail + config_present=false + token_present=false + [[ -f qodana-global-configurations.yaml ]] && config_present=true + [[ -n "${QODANA_CONFIGURATIONS_TOKEN:-}" ]] && token_present=true + + if [[ "$config_present" == true && "$token_present" == true ]]; then + echo "ready=true" >> "$GITHUB_OUTPUT" + echo "reason=all prerequisites are configured" >> "$GITHUB_OUTPUT" + elif [[ "$config_present" != true && "$token_present" != true ]]; then + echo "ready=false" >> "$GITHUB_OUTPUT" + echo "reason=qodana-global-configurations.yaml and QODANA_CONFIGURATIONS_TOKEN are missing" >> "$GITHUB_OUTPUT" + elif [[ "$config_present" != true ]]; then + echo "ready=false" >> "$GITHUB_OUTPUT" + echo "reason=qodana-global-configurations.yaml is missing" >> "$GITHUB_OUTPUT" + else + echo "ready=false" >> "$GITHUB_OUTPUT" + echo "reason=QODANA_CONFIGURATIONS_TOKEN is missing" >> "$GITHUB_OUTPUT" + fi + - name: Run Qodana Configuration Uploader + if: ${{ steps.qodana-upload-prerequisites.outputs.ready == 'true' }} env: QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }} run: | @@ -30,3 +57,9 @@ jobs: jetbrains/qodana-configuration-uploader@sha256:f4786ceea616048c3401cf0b0345d2220d22a2ec7b046fd48cbbfc522e6efe30 \ --global-configs-file qodana-global-configurations.yaml \ --qodana-host https://qodana.cloud + + - name: Skip Qodana Configuration Upload + if: ${{ steps.qodana-upload-prerequisites.outputs.ready != 'true' }} + env: + QODANA_SKIP_REASON: ${{ steps.qodana-upload-prerequisites.outputs.reason }} + run: echo "Skipping Qodana configuration upload because ${QODANA_SKIP_REASON}." diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index a49f0970..4ac00f36 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -48,8 +48,8 @@ permissions: contents: read concurrency: - group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.head_ref || github.ref_name }} - cancel-in-progress: true + group: frontend-tests-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} # Repository variables used as CI runner and credit controls: # - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs. @@ -511,6 +511,31 @@ jobs: if-no-files-found: ignore retention-days: 1 + required-ci: + if: ${{ always() && (github.event_name == 'pull_request' || github.event_name == 'push') }} + name: Required CI + needs: [format-tests, build-and-unit, e2e-pr] + runs-on: ubuntu-latest + timeout-minutes: 5 + steps: + - name: Verify required jobs succeeded + shell: bash + env: + FORMAT_TESTS_RESULT: ${{ needs.format-tests.result }} + BUILD_AND_UNIT_RESULT: ${{ needs.build-and-unit.result }} + E2E_PR_RESULT: ${{ needs.e2e-pr.result }} + run: | + set -euo pipefail + failed=0 + for required_job in FORMAT_TESTS_RESULT BUILD_AND_UNIT_RESULT E2E_PR_RESULT; do + result="${!required_job:-missing}" + if [[ "$result" != "success" ]]; then + echo "${required_job}=${result}" >&2 + failed=1 + fi + done + exit "$failed" + e2e-full: if: > always() && diff --git a/README.md b/README.md index acc51a0e..1b3f84a7 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,16 @@ See [Vite Configuration Reference](https://vite.dev/config/). npm install ``` +## Contributing Changes + +Create a scoped feature branch, push it, and open a pull request targeting +`master`. Do not push directly to `master`. Merge only after the `Required CI` +check succeeds, all review conversations are resolved, and the branch is up to +date. Use squash merge so `master` retains linear history. + +See [`.github/BRANCH_PROTECTION.md`](.github/BRANCH_PROTECTION.md) for the +repository policy, rollout checks, and emergency bypass procedure. + ### Compile and Hot-Reload for Development ```sh diff --git a/scripts/run-playwright-full-slice.mjs b/scripts/run-playwright-full-slice.mjs index a09c389b..f8687d5f 100644 --- a/scripts/run-playwright-full-slice.mjs +++ b/scripts/run-playwright-full-slice.mjs @@ -100,6 +100,7 @@ export const ownedFilesByRole = { "session-bootstrap.spec.ts", "superuser-bookings.spec.ts", "superuser-cron.spec.ts", + "superuser-customer-rules.spec.ts", "superuser-customer-complaints.spec.ts", "superuser-customers-mass-import.spec.ts", "superuser-department-branding.spec.js",