From b30637ee4efb7e571ebfcd3f10bec830bff36119 Mon Sep 17 00:00:00 2001 From: Per Magnus Petersen Date: Thu, 17 Sep 2026 14:00:26 +0200 Subject: [PATCH] Add pull request deployment workflow --- .gitea/workflows/pr.yml | 20 ++++ PR-DEPLOYMENT.md | 219 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) create mode 100644 .gitea/workflows/pr.yml create mode 100644 PR-DEPLOYMENT.md diff --git a/.gitea/workflows/pr.yml b/.gitea/workflows/pr.yml new file mode 100644 index 0000000..5f2c581 --- /dev/null +++ b/.gitea/workflows/pr.yml @@ -0,0 +1,20 @@ +name: pull-request + +on: + pull_request: + branches: [main] + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: '3.13' + + - name: Test + run: | + python -m pip install -r requirements-dev.txt + python -m pytest tests/ diff --git a/PR-DEPLOYMENT.md b/PR-DEPLOYMENT.md new file mode 100644 index 0000000..1b69c0d --- /dev/null +++ b/PR-DEPLOYMENT.md @@ -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-` and SHA-qualified tags to + `harbor.pmagnus.cloud/btcprice/btcprice`. +- Flux watches the plain `main-` 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/ +``` + +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/ +tea pulls create \ + --repo magnus/btcprice \ + --base main \ + --head feature/ \ + --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-` and `main--`. +3. Harbor scans the image and permits the companion tag after policy approval. +4. Flux image reflection selects the newest plain `main-` 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.