Add pull request deployment workflow
build / build (push) Successful in 2m43s

This commit is contained in:
2026-09-17 14:00:26 +02:00
parent 6c21f172db
commit b30637ee4e
2 changed files with 239 additions and 0 deletions
+219
View File
@@ -0,0 +1,219 @@
# Pull-request deployment workflow
## Intended result
Production deployment should happen only after a change has followed this path:
```text
feature branch
-> local test and local application run
-> pull request with automated checks
-> owner reviews the result and approves by merging
-> merge to main
-> Gitea Actions builds and publishes an immutable image
-> Flux updates magnus/deploy
-> Flux reconciles Kubernetes
-> production verification
```
For this single-user repository, clicking **Merge** after reviewing the diff and successful checks
is the approval. The merge creates the `push` event which starts the existing production build.
## Findings (2026-09-17)
- The default branch is `main`.
- Gitea reports no branch-protection rule for this repository. A contributor can therefore push
directly to `main`, bypassing a pull request and approval.
- `.gitea/workflows/pr.yml` now runs tests for pull requests targeting `main`; it has no publish
step and receives no Harbor credentials.
- `.gitea/workflows/build.yml` remains restricted to pushes to `main` and `v*` tags. A merged PR
therefore starts the existing image publication and automatic Kubernetes deployment path.
- Recent `main` workflow runs show that the existing post-merge build and publish workflow works.
- The workflow runs `pytest`, builds a linux/amd64 image, and publishes immutable
`main-<UTC timestamp>` and SHA-qualified tags to
`harbor.pmagnus.cloud/btcprice/btcprice`.
- Flux watches the plain `main-<UTC timestamp>` tags. Its image automation writes the selected tag
into `magnus/deploy`, after which Flux reconciles the Kubernetes cluster.
- Kubernetes runs two replicas with rolling updates, health/readiness probes, and one pod per node.
- Gitea currently has only one user (`magnus`). Mandatory reviewer approval is therefore not
appropriate: Gitea does not provide meaningful independent self-approval. The owner's explicit
merge action is the approval, while branch protection and CI remain the enforceable gates.
## Required one-time configuration
### 1. Pull-request validation workflow (implemented)
`.gitea/workflows/pr.yml` validates every pull request targeting `main`. It checks out the proposed
commit and runs the tests without publishing an image or receiving Harbor credentials. The
production Docker image is built locally before the PR and again by `build.yml` after merge.
```bash
python -m pip install -r requirements-dev.txt
python -m pytest tests/
```
The contributor performs the Docker build locally before opening the PR. Keeping it out of the PR
job avoids giving proposed code credentials for the private Harbor base images. After merge, the
trusted `build.yml` workflow builds the production image with Harbor credentials.
Keep image publication restricted to the existing `push` event for `main` and `v*` tags. PR code,
especially code from an untrusted fork, must not receive the Harbor credentials.
The workflows are intentionally separated:
- `pr.yml`: `pull_request` trigger; tests only; no secrets and no push.
- `build.yml`: existing `push` trigger; tests, image build, Harbor scan gate, and image push.
### 2. Protect `main` in Gitea (configuration still required)
In `magnus/btcprice`, open **Settings -> Branches -> Branch protection rules** and protect `main`.
Configure the rule to:
- disable direct pushes (and force pushes);
- require pull requests before merge;
- set required approvals to zero while the repository has only one user;
- block merge when a review requests changes;
- require the PR validation status check;
- require the branch to be up to date before merge;
- restrict bypass/whitelist permissions to emergency administrators only.
Create the PR workflow and let it run once before selecting its status-check context in the branch
rule; this avoids guessing Gitea's generated context name. With required approvals set to zero, the
owner reviews the diff and approves it by clicking **Merge** after the required check passes. If a
second trusted user is added later, change the rule to require one approval and dismiss stale
approvals when new commits are pushed.
## Per-change procedure
### 1. Create a feature branch
Start from the latest production branch:
```bash
git switch main
git pull --ff-only
git switch -c feature/<short-description>
```
Make and commit the change on this branch. Do not commit `.env`; it contains local configuration.
### 2. Test and run the branch locally before opening the PR
Run the automated tests:
```bash
make test
```
For a functional check, start `btcdata` in one terminal:
```bash
cd ../btcdata
make run
```
Then start this branch of `btcprice` in another terminal:
```bash
cd ../btcprice
make run
```
Open `http://localhost:4000` and confirm that both live price streams update. Also check the
application endpoints:
```bash
curl --fail http://localhost:4000/api/health
curl --fail http://localhost:4000/api/ready
curl --fail http://localhost:4000/api/version
```
`/api/health` checks the btcprice process; `/api/ready` also verifies that btcdata is reachable.
The local version normally reports `development`.
Finally, exercise the production build path:
```bash
docker build \
--build-arg BUILD_SHA="$(git rev-parse --short HEAD)" \
-t btcprice:pr-check .
```
If Docker validation needs the complete local stack, create `btc-network` once, start btcdata's
Compose project, and run `docker compose up --build` in this repository. This application then
listens at `http://localhost:4041`.
Record the tests and manual behavior checked in the PR description.
### 3. Push and open the pull request
```bash
git push -u origin feature/<short-description>
tea pulls create \
--repo magnus/btcprice \
--base main \
--head feature/<short-description> \
--title "<concise change title>"
```
The PR should explain the change, risk, local verification, and rollback considerations. Wait for
the PR validation job to pass, then review and merge it. Any new commit triggers validation again.
### 4. Approve and merge
As repository owner, check the diff, confirm the automated result, and verify the stated manual
test. Clicking **Merge** is the approval step in this single-user setup. Prefer **squash merge** for
small, single-purpose PRs or use another consistent merge policy.
Do not press merge if the check is failing or the PR branch is behind `main`. Do not deploy with
`kubectl apply` or `deploy-citadel.sh`; those bypass the GitOps audit trail.
### 5. Observe the deployment
After merge, observe this sequence:
1. Gitea Actions tests the merge commit.
2. It publishes `main-<UTC timestamp>` and `main-<UTC timestamp>-<short SHA>`.
3. Harbor scans the image and permits the companion tag after policy approval.
4. Flux image reflection selects the newest plain `main-<UTC timestamp>` tag.
5. Flux commits the updated image reference to `magnus/deploy`.
6. Flux applies that Git state and Kubernetes performs a rolling update.
The CI job intentionally has no Kubernetes credentials. A successful image build is not by itself
proof of a successful rollout, so verify production separately:
```bash
curl --fail https://btcprice.k8s.pmagnus.cloud/api/health
curl --fail https://btcprice.k8s.pmagnus.cloud/api/ready
curl --fail https://btcprice.k8s.pmagnus.cloud/api/version
```
The production version should equal the short SHA of the merged btcprice commit. Also confirm in
the deployment repository or cluster that both replicas became Ready. If rollout access is
available, useful checks are:
```bash
flux get kustomizations
kubectl -n btcprice rollout status deployment/btcprice
kubectl -n btcprice get pods -o wide
```
## Failure and rollback behavior
- If PR validation fails, fix the feature branch; nothing has been published or deployed.
- If the post-merge build fails, Kubernetes keeps running the previous image. Fix forward through a
new PR; do not manually push a replacement under an existing immutable tag.
- If Harbor rejects the image, Flux cannot select it and production stays on the previous image.
- If a new image deploys but is bad, revert the Flux image commit in `magnus/deploy`, or commit the
last known-good immutable tag there. Flux then restores that version. Follow up with a revert or
fix PR in this repository so future automation does not reintroduce the bad build.
## Security boundary
The separation is intentional:
- PR jobs validate untrusted proposed code without deployment or registry credentials.
- The protected `main` branch is the release authorization boundary.
- Gitea Actions may write images to Harbor only after merge.
- Flux alone has authority to update the GitOps repository and reconcile Kubernetes.
- Kubernetes changes remain reviewable and recoverable through Git history.