220 lines
8.6 KiB
Markdown
220 lines
8.6 KiB
Markdown
# 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.
|