Prepare frontend master branch protection (#172)
Add stable Required CI, repair E2E ownership, make Qodana optional-state handling explicit, and document the desired protection policy.
This commit is contained in:
@@ -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.
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -2,9 +2,9 @@ name: Qodana Configuration Upload
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main, dev]
|
branches: [master, beta, canary, internal]
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [main]
|
branches: [master, beta, canary, internal]
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -19,7 +19,34 @@ jobs:
|
|||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v5
|
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
|
- name: Run Qodana Configuration Uploader
|
||||||
|
if: ${{ steps.qodana-upload-prerequisites.outputs.ready == 'true' }}
|
||||||
env:
|
env:
|
||||||
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
|
QODANA_CONFIGURATIONS_TOKEN: ${{ secrets.QODANA_CONFIGURATIONS_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
@@ -30,3 +57,9 @@ jobs:
|
|||||||
jetbrains/qodana-configuration-uploader@sha256:f4786ceea616048c3401cf0b0345d2220d22a2ec7b046fd48cbbfc522e6efe30 \
|
jetbrains/qodana-configuration-uploader@sha256:f4786ceea616048c3401cf0b0345d2220d22a2ec7b046fd48cbbfc522e6efe30 \
|
||||||
--global-configs-file qodana-global-configurations.yaml \
|
--global-configs-file qodana-global-configurations.yaml \
|
||||||
--qodana-host https://qodana.cloud
|
--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}."
|
||||||
|
|||||||
@@ -48,8 +48,8 @@ permissions:
|
|||||||
contents: read
|
contents: read
|
||||||
|
|
||||||
concurrency:
|
concurrency:
|
||||||
group: frontend-tests-${{ github.workflow }}-${{ github.event_name }}-${{ github.event_name == 'workflow_dispatch' && github.run_id || github.head_ref || github.ref_name }}
|
group: frontend-tests-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.event.pull_request.number || github.run_id }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||||
|
|
||||||
# Repository variables used as CI runner and credit controls:
|
# Repository variables used as CI runner and credit controls:
|
||||||
# - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs.
|
# - FRONTEND_CI_STANDARD_RUNNER: JSON runs-on value for format/build/unit jobs.
|
||||||
@@ -511,6 +511,31 @@ jobs:
|
|||||||
if-no-files-found: ignore
|
if-no-files-found: ignore
|
||||||
retention-days: 1
|
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:
|
e2e-full:
|
||||||
if: >
|
if: >
|
||||||
always() &&
|
always() &&
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ See [Vite Configuration Reference](https://vite.dev/config/).
|
|||||||
npm install
|
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
|
### Compile and Hot-Reload for Development
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
|
|||||||
@@ -100,6 +100,7 @@ export const ownedFilesByRole = {
|
|||||||
"session-bootstrap.spec.ts",
|
"session-bootstrap.spec.ts",
|
||||||
"superuser-bookings.spec.ts",
|
"superuser-bookings.spec.ts",
|
||||||
"superuser-cron.spec.ts",
|
"superuser-cron.spec.ts",
|
||||||
|
"superuser-customer-rules.spec.ts",
|
||||||
"superuser-customer-complaints.spec.ts",
|
"superuser-customer-complaints.spec.ts",
|
||||||
"superuser-customers-mass-import.spec.ts",
|
"superuser-customers-mass-import.spec.ts",
|
||||||
"superuser-department-branding.spec.js",
|
"superuser-department-branding.spec.js",
|
||||||
|
|||||||
Reference in New Issue
Block a user