Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@ version: 2
updates:
- package-ecosystem: github-actions
directory: /
# Updates are tested in staging like every other change; main only takes
# pull requests from staging.
target-branch: staging
schedule:
interval: monthly
commit-message:
Expand Down
2 changes: 2 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
<!-- Open this against staging. main only accepts pull requests from staging; see CONTRIBUTING.md. -->

## What this changes

<!-- One or two sentences. What is different in the game after this merges? -->
Expand Down
42 changes: 42 additions & 0 deletions .github/rulesets/main.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
{
"name": "Protect main",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/heads/main"],
"exclude": []
}
},
"bypass_actors": [],
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"allowed_merge_methods": ["merge"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"required_status_checks": [
{ "context": "Only staging merges into main" },
{ "context": "Linux Debug" },
{ "context": "Linux Release" },
{ "context": "Windows Debug" },
{ "context": "Windows Release" },
{ "context": "Shaders compile and are committed" },
{ "context": "Headless smoke test" }
]
}
}
]
}
41 changes: 41 additions & 0 deletions .github/rulesets/staging.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
{
"name": "Protect staging",
"target": "branch",
"enforcement": "active",
"conditions": {
"ref_name": {
"include": ["refs/heads/staging"],
"exclude": []
}
},
"bypass_actors": [],
"rules": [
{ "type": "deletion" },
{ "type": "non_fast_forward" },
{
"type": "pull_request",
"parameters": {
"required_approving_review_count": 0,
"dismiss_stale_reviews_on_push": false,
"require_code_owner_review": false,
"require_last_push_approval": false,
"required_review_thread_resolution": false,
"allowed_merge_methods": ["merge", "squash", "rebase"]
}
},
{
"type": "required_status_checks",
"parameters": {
"strict_required_status_checks_policy": false,
"required_status_checks": [
{ "context": "Linux Debug" },
{ "context": "Linux Release" },
{ "context": "Windows Debug" },
{ "context": "Windows Release" },
{ "context": "Shaders compile and are committed" },
{ "context": "Headless smoke test" }
]
}
}
]
}
43 changes: 43 additions & 0 deletions .github/workflows/branch-policy.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: Branch policy

# staging is the testing layer: every change lands there first, and main only
# takes pull requests from staging. This check fails any other pull request
# into main. It also runs on pull requests into staging, where it always
# passes, so retargeting a pull request from main to staging clears the
# failure it picked up there.
#
# pull_request_target runs this file as it exists on the base branch, so a
# pull request cannot edit the check into passing. Nothing from the pull
# request is checked out or run, and the job holds no token permissions.
on:
pull_request_target:
branches: [main, staging]
types: [opened, reopened, synchronize, edited]

permissions: {}

jobs:
# The job name is a required status check in .github/rulesets/main.json.
# Keep the verdict inside the step: a job skipped by an `if:` reports
# success, which would satisfy the required check.
staging-to-main:
name: Only staging merges into main
runs-on: ubuntu-latest
steps:
- name: Check where the pull request comes from
env:
BASE_REF: ${{ github.event.pull_request.base.ref }}
HEAD_REF: ${{ github.event.pull_request.head.ref }}
HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
run: |
if [ "$BASE_REF" != "main" ]; then
echo "Targets $BASE_REF, which accepts pull requests from any branch."
exit 0
fi
if [ "$HEAD_REPO" = "$GITHUB_REPOSITORY" ] && [ "$HEAD_REF" = "staging" ]; then
echo "Release pull request from staging into main."
exit 0
fi
echo "This pull request comes from ${HEAD_REPO:-a deleted fork}:${HEAD_REF}."
echo "::error::main only accepts pull requests from staging. Edit this pull request and change its base branch to staging."
exit 1
19 changes: 17 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ name: CI

on:
push:
branches: [main]
branches: [main, staging]
pull_request:
branches: [main]
branches: [main, staging]
workflow_dispatch:
inputs:
engine_ref:
Expand All @@ -19,6 +19,9 @@ concurrency:
permissions:
contents: read

# The check names here (the build matrix names and the other jobs' names) are
# required status checks in .github/rulesets/. Rename both together, or pull
# requests will wait on a check that never reports.
jobs:
build:
name: ${{ matrix.name }}
Expand Down Expand Up @@ -142,6 +145,18 @@ jobs:
fi
echo "Built $found"

# staging is the testing layer, so every push there leaves a playable
# build to try before staging is merged into main. bin/ holds the binary
# next to its assets, the same folder a release packages.
- name: Upload the staging test build
if: github.event_name == 'push' && github.ref == 'refs/heads/staging' && matrix.config == 'Release'
uses: actions/upload-artifact@v4
with:
name: SleakCraft-staging-${{ runner.os }}
retention-days: 14
if-no-files-found: error
path: SleakCraft/bin/

shaders:
name: Shaders compile and are committed
runs-on: ubuntu-latest
Expand Down
2 changes: 2 additions & 0 deletions .github/workflows/engine-update.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ on:

permissions:
contents: read
# The failure step below opens an issue.
issues: write

jobs:
verify:
Expand Down
54 changes: 54 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Contributing

## Branches

`staging` is the testing layer between a change and `main`. Everything lands in `staging` first, gets tested there, and reaches `main` only when `staging` is merged into it.

| Branch | Holds | Accepts |
|--------|-------|---------|
| `staging` | Changes being tested | Pull requests from any branch, once CI passes |
| `main` | Tested, released code | Pull requests from `staging` only, once CI passes |

Open every pull request against `staging`. A pull request into `main` from any other branch fails the **Only staging merges into main** check and cannot be merged. To fix it, edit the pull request and change its base branch to `staging`.

---

## Testing in staging

Every pull request into `staging` runs the full CI: the Linux and Windows build matrix, the shader check, and the headless smoke test.

Every push to `staging` also leaves a playable Release build. Open the CI run for the latest `staging` commit in the Actions tab and download `SleakCraft-staging-Windows` or `SleakCraft-staging-Linux` from the bottom of the page. The builds are kept for 14 days.

---

## Releasing staging to main

1. Open a pull request from `staging` into `main`.
2. Wait for CI to pass.
3. Merge it with **Create a merge commit**.

`main` only allows merge commits. Squash and rebase copy staging's commits into new ones, so the two branches would stop sharing history, and every later release would replay old commits and hit conflicts that aren't real.

After a release, GitHub shows `staging` as behind `main`. The commits it is missing are the release merge commits, which change nothing on their own and need no action.

---

## Branch protection

The rules are the JSON files in `.github/rulesets/`, which GitHub imports as they are: **Settings → Rules → Rulesets → New ruleset → Import a ruleset**.

| Rule | `main` | `staging` |
|------|--------|-----------|
| Changes arrive by pull request only | ✓ | ✓ |
| No force pushes, no deletion | ✓ | ✓ |
| CI must pass | ✓ | ✓ |
| Source must be `staging` | ✓ | |
| Merge method | Merge commit | Any |

> **Setting it up?** Import `main.json` only after `.github/workflows/branch-policy.yml` is on `main`. The ruleset requires the check that workflow reports, so until the workflow exists nothing can merge into `main`. **Allow merge commits** must stay on under Settings → General → Pull Requests.

Neither ruleset requires an approving review, because GitHub does not let authors approve their own pull requests. Raise `required_approving_review_count` once there is a second maintainer.

Nobody can bypass the rules, admins included. To get out of a jam, set the ruleset's enforcement to **Disabled**, fix the problem, then set it back to **Active**.

Required checks are matched by name, so renaming a job in `ci.yml` or `branch-policy.yml` means updating the rulesets to match.
Loading