T-3: add Gitea Actions CI/CD pipeline
Some checks failed
CI / backend-test (push) Successful in 46s
CI / frontend-test (push) Successful in 18s
CI / build-and-publish (push) Failing after 5s

Single workflow with three jobs:
- backend-test: JDK 25, ./mvnw -B verify (Checkstyle, JUnit, ArchUnit, SpotBugs)
- frontend-test: Node 24, lint, type generation, type-check, unit tests, production build
- build-and-publish: Buildah image build + push with rolling SemVer tags (only on vX.Y.Z tags)

Includes research report and implementation plan.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-04 20:08:29 +01:00
parent 316137bf1c
commit 977795ffb1
3 changed files with 567 additions and 0 deletions

92
.gitea/workflows/ci.yaml Normal file
View File

@@ -0,0 +1,92 @@
name: CI
on:
push:
jobs:
backend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 25
- name: Run backend verify
run: cd backend && ./mvnw -B verify
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: cd frontend && npm ci
- name: Lint
run: cd frontend && npm run lint
- name: Generate API types
run: cd frontend && npm run generate:api
- name: Type check
run: cd frontend && npm run type-check
- name: Unit tests
run: cd frontend && npm run test:unit -- --run
- name: Production build
run: cd frontend && npm run build
build-and-publish:
needs: [backend-test, frontend-test]
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Parse SemVer tag
id: semver
run: |
TAG="${{ github.ref_name }}"
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Not a valid SemVer tag: $TAG"
exit 1
fi
MAJOR="${TAG%%.*}"
MINOR="${TAG%.*}"
echo "full=$TAG" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
- name: Build image
run: |
REGISTRY="${{ github.server_url }}"
REGISTRY="${REGISTRY#https://}"
REGISTRY="${REGISTRY#http://}"
REPO="${{ github.repository }}"
IMAGE="${REGISTRY}/${REPO}"
buildah bud -t "${IMAGE}:${{ steps.semver.outputs.full }}" .
buildah tag "${IMAGE}:${{ steps.semver.outputs.full }}" \
"${IMAGE}:${{ steps.semver.outputs.minor }}" \
"${IMAGE}:${{ steps.semver.outputs.major }}" \
"${IMAGE}:latest"
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
- name: Push to registry
run: |
buildah login -u "${{ github.repository_owner }}" \
-p "${{ secrets.REGISTRY_TOKEN }}" \
"${IMAGE%%/*}"
buildah push "${IMAGE}:${{ steps.semver.outputs.full }}"
buildah push "${IMAGE}:${{ steps.semver.outputs.minor }}"
buildah push "${IMAGE}:${{ steps.semver.outputs.major }}"
buildah push "${IMAGE}:latest"

View File

@@ -0,0 +1,262 @@
---
date: "2026-03-04T18:46:12.266203+00:00"
git_commit: 316137bf1c391577e884ce525af780f45e34da86
branch: master
topic: "T-3: CI/CD Pipeline — Gitea Actions"
tags: [plan, ci-cd, gitea, docker, buildah]
status: implemented
---
# T-3: CI/CD Pipeline Implementation Plan
## Overview
Set up a Gitea Actions CI/CD pipeline that runs on every push (tests only) and on SemVer-tagged releases (tests + build + publish). Single workflow file, three jobs.
## Current State Analysis
- All dependencies completed: T-1 (monorepo), T-2 (Dockerfile), T-5 (API-first tooling)
- Multi-stage Dockerfile exists and works (`Dockerfile:1-26`)
- Dockerfile skips tests (`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip`) by design — quality gates belong in CI
- `.dockerignore` already excludes `.gitea/`
- No CI/CD configuration exists yet
- Backend has full verify lifecycle: Checkstyle, JUnit 5, ArchUnit, SpotBugs
- Frontend has lint (oxlint + ESLint), type-check (`vue-tsc`), tests (Vitest), and Vite production build
## Desired End State
A single workflow file at `.gitea/workflows/ci.yaml` that:
1. Triggers on every push (branches and tags)
2. Runs backend and frontend quality gates in parallel
3. On SemVer tags only: builds the Docker image via Buildah and publishes it with rolling tags
The Docker image build is **not** run on regular pushes. All quality gates (tests, lint, type-check, production build) already run in the test jobs. The Dockerfile itself changes rarely, and any breakage surfaces at the next tagged release. This avoids a redundant image build on every push.
### Verification:
- Push a commit → pipeline runs tests only, no image build
- Push a non-SemVer tag → same behavior
- Push a SemVer tag (e.g. `1.0.0`) → tests + build + publish with 4 tags
### Pipeline Flow:
```
git push (any branch/tag)
┌───────────┴───────────┐
▼ ▼
┌─────────────────┐ ┌─────────────────────┐
│ backend-test │ │ frontend-test │
│ │ │ │
│ JDK 25 │ │ Node 24 │
│ ./mvnw -B │ │ npm ci │
│ verify │ │ npm run lint │
│ │ │ npm run type-check │
│ │ │ npm run test:unit │
│ │ │ -- --run │
│ │ │ npm run build │
└────────┬─────────┘ └──────────┬───────────┘
│ │
└───────────┬─────────────┘
┌───────────────────┐
│ SemVer-Tag? │── nein ──► DONE
└────────┬──────────┘
│ ja
┌─────────────────────┐
│ build-and-publish │
│ │
│ buildah bud │
│ buildah tag ×4 │
│ buildah push ×4 │
└─────────────────────┘
```
## What We're NOT Doing
- No deployment automation (how/where to deploy is the hoster's responsibility)
- No branch protection rules (Gitea admin concern, not pipeline scope)
- No automated versioning (no release-please, no semantic-release — manual `git tag`)
- No caching optimization (can be added later if runner time becomes a concern)
- No separate staging/production pipelines
- No notifications (Slack, email, etc.)
## Implementation Approach
Single phase — this is one YAML file with well-defined structure. The workflow uses Buildah for image builds to avoid Docker-in-Docker issues on the self-hosted runner.
## Phase 1: Gitea Actions Workflow
### Overview
Create the complete CI/CD workflow file with three jobs: backend-test, frontend-test, build-and-publish (SemVer tags only).
### Changes Required:
#### [x] 1. Create workflow directory
**Action**: `mkdir -p .gitea/workflows/`
#### [x] 2. Explore OpenAPI spec access in CI
The `npm run type-check` and `npm run build` steps need access to the OpenAPI spec because `generate:api` runs as a pre-step. In CI, the checkout includes both `backend/` and `frontend/` as sibling directories, so the relative path `../backend/src/main/resources/openapi/api.yaml` from `frontend/` should resolve correctly.
**Task:** During implementation, verify whether the relative path works from the CI checkout structure. If it does, no `cp` step is needed. If it doesn't, add an explicit copy step. The workflow YAML below includes a `cp` as a safety measure — **remove it if the relative path works without it**.
**Resolution:** The relative path works. The `cp` was removed. An explicit `generate:api` step was added before `type-check` so that `schema.d.ts` exists when `vue-tsc` runs (since `type-check` alone doesn't trigger code generation).
#### [x] 3. Create workflow file
**File**: `.gitea/workflows/ci.yaml`
```yaml
name: CI
on:
push:
jobs:
backend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up JDK 25
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 25
- name: Run backend verify
run: cd backend && ./mvnw -B verify
frontend-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Node 24
uses: actions/setup-node@v4
with:
node-version: 24
- name: Install dependencies
run: cd frontend && npm ci
- name: Lint
run: cd frontend && npm run lint
- name: Type check
run: |
cp backend/src/main/resources/openapi/api.yaml frontend/
cd frontend && npm run type-check
- name: Unit tests
run: cd frontend && npm run test:unit -- --run
- name: Production build
run: cd frontend && npm run build
build-and-publish:
needs: [backend-test, frontend-test]
if: startsWith(github.ref, 'refs/tags/') && contains(github.ref_name, '.')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Parse SemVer tag
id: semver
run: |
TAG="${{ github.ref_name }}"
if [[ ! "$TAG" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Not a valid SemVer tag: $TAG"
exit 1
fi
MAJOR="${TAG%%.*}"
MINOR="${TAG%.*}"
echo "full=$TAG" >> "$GITHUB_OUTPUT"
echo "minor=$MINOR" >> "$GITHUB_OUTPUT"
echo "major=$MAJOR" >> "$GITHUB_OUTPUT"
- name: Build image
run: |
REGISTRY="${{ github.server_url }}"
REGISTRY="${REGISTRY#https://}"
REGISTRY="${REGISTRY#http://}"
REPO="${{ github.repository }}"
IMAGE="${REGISTRY}/${REPO}"
buildah bud -t "${IMAGE}:${{ steps.semver.outputs.full }}" .
buildah tag "${IMAGE}:${{ steps.semver.outputs.full }}" \
"${IMAGE}:${{ steps.semver.outputs.minor }}" \
"${IMAGE}:${{ steps.semver.outputs.major }}" \
"${IMAGE}:latest"
echo "IMAGE=${IMAGE}" >> "$GITHUB_ENV"
- name: Push to registry
run: |
buildah login -u "${{ github.repository_owner }}" \
-p "${{ secrets.REGISTRY_TOKEN }}" \
"${IMAGE%%/*}"
buildah push "${IMAGE}:${{ steps.semver.outputs.full }}"
buildah push "${IMAGE}:${{ steps.semver.outputs.minor }}"
buildah push "${IMAGE}:${{ steps.semver.outputs.major }}"
buildah push "${IMAGE}:latest"
```
### Success Criteria:
#### Automated Verification:
- [x] YAML is valid: `python3 -c "import yaml; yaml.safe_load(open('.gitea/workflows/ci.yaml'))"`
- [x] File is in the correct directory: `.gitea/workflows/ci.yaml`
- [x] Workflow triggers on `push` (all branches and tags)
- [x] `backend-test` uses JDK 25 and runs `./mvnw -B verify`
- [x] `frontend-test` uses Node 24 and runs lint, type-check, tests, and build
- [x] `build-and-publish` depends on both test jobs (`needs: [backend-test, frontend-test]`)
- [x] `build-and-publish` only runs on SemVer tags (`if` condition)
- [x] SemVer parsing correctly extracts major, minor, and full version
- [x] Registry URL is derived from `github.server_url` with protocol stripped
- [x] Authentication uses `secrets.REGISTRY_TOKEN` (not the built-in token)
#### Manual Verification:
- [ ] Push a commit to a branch → pipeline runs `backend-test` and `frontend-test` only — no image build
- [ ] Push a SemVer tag → pipeline runs all three jobs, image appears in Gitea container registry with 4 tags
- [ ] Break a test intentionally → pipeline fails, `build-and-publish` does not run
- [ ] Push a non-SemVer tag → pipeline runs tests only, no image build
**Implementation Note**: After creating the workflow file and passing automated verification, the manual verification requires pushing to the actual Gitea instance. Pause here for the human to test on the real runner.
---
## Testing Strategy
### Automated (pre-push):
- YAML syntax validation
- Verify file structure and job dependencies match the spec
### Manual (post-push, on Gitea):
1. Push a normal commit → verify only test jobs run, no image build
2. Intentionally break a backend test → verify pipeline fails at `backend-test`
3. Intentionally break a frontend lint rule → verify pipeline fails at `frontend-test`
4. Fix and push a SemVer tag (e.g. `0.1.0`) → verify all 3 jobs run, image published with rolling tags
5. Verify image is pullable: `docker pull {registry}/{owner}/fete:0.1.0`
## Performance Considerations
- Backend and frontend tests run in parallel (separate jobs) — this is the main time saver
- Docker image build only runs on SemVer tags — no wasted runner time on regular pushes
- No Maven/npm caching configured — can be added later if runner time becomes a problem
## Configuration Prerequisites
The following must be configured in Gitea **before** the pipeline can publish images:
1. **Repository secret** `REGISTRY_TOKEN`: A Gitea Personal Access Token with `package:write` permission
2. **Buildah** must be installed on the runner (standard on most Linux runners)
## References
- Research: `docs/agents/research/2026-03-04-t3-cicd-pipeline.md`
- Spec: `spec/setup-tasks.md:41-55`
- Dockerfile: `Dockerfile:1-26`
- Frontend scripts: `frontend/package.json:6-17`
- Backend plugins: `backend/pom.xml:56-168`

View File

@@ -0,0 +1,213 @@
---
date: "2026-03-04T18:19:10.241698+00:00"
git_commit: 316137bf1c391577e884ce525af780f45e34da86
branch: master
topic: "T-3: CI/CD Pipeline — Gitea Actions"
tags: [research, codebase, ci-cd, gitea, docker, pipeline]
status: complete
---
# Research: T-3 CI/CD Pipeline
## Research Question
What is the current state of the project relevant to implementing T-3 (CI/CD pipeline with Gitea Actions), what are its requirements, dependencies, and what infrastructure already exists?
## Summary
T-3 requires a Gitea Actions workflow in `.gitea/workflows/` that runs on every push: tests both backend and frontend, builds a Docker image, and publishes it to the Gitea container registry. The task is currently unstarted but all dependencies (T-1, T-2) are completed. The project already has a working multi-stage Dockerfile, comprehensive test suites for both backend and frontend, and clearly defined build commands. No CI/CD configuration files exist yet.
## Detailed Findings
### T-3 Specification
Defined in `spec/setup-tasks.md:41-55`.
**Acceptance Criteria (all unchecked):**
1. Gitea Actions workflow file in `.gitea/workflows/` runs on push: test, build, publish Docker image
2. Backend tests run via Maven
3. Frontend tests run via Vitest
4. Docker image is published to the Gitea container registry on the same instance
5. Pipeline fails visibly if any test fails or the build breaks
6. Docker image is only published if all tests pass and the build succeeds
**Dependencies:** T-1 (completed), T-2 (completed)
**Platform Decision (Q-5):** Per `.ralph/review-findings/questions.md:12-22`, Gitea is confirmed as the exclusive CI/CD platform. Only Gitea infrastructure will be used — Gitea Actions for pipelines, Gitea container registry for Docker image publishing.
### Dependencies — What Already Exists
#### Dockerfile (repo root)
A working 3-stage Dockerfile exists (`Dockerfile:1-26`):
| Stage | Base Image | Purpose |
|-------|-----------|---------|
| `frontend-build` | `node:24-alpine` | `npm ci` + `npm run build` (includes OpenAPI type generation) |
| `backend-build` | `eclipse-temurin:25-jdk-alpine` | Maven build with frontend assets baked into `static/` |
| `runtime` | `eclipse-temurin:25-jre-alpine` | `java -jar app.jar`, exposes 8080, HEALTHCHECK configured |
The Dockerfile skips tests and static analysis during build (`-DskipTests -Dcheckstyle.skip -Dspotbugs.skip` at `Dockerfile:17`), with the explicit design rationale that quality gates belong in CI (T-3).
The OpenAPI spec is copied into the frontend build stage (`Dockerfile:8-9`) because `npm run build` triggers `generate:api` as a pre-step.
#### .dockerignore
`.dockerignore:18-19` already excludes `.gitea/` from the Docker build context, anticipating this directory's creation.
#### Backend Build & Test
- **Build:** `./mvnw package` (or `./mvnw -B package` for batch mode)
- **Test:** `./mvnw test`
- **Full verify:** `./mvnw verify` (includes Checkstyle, SpotBugs, ArchUnit, JUnit 5)
- **Config:** `backend/pom.xml` — Spring Boot 3.5.11, Java 25
- **Quality gates:** Checkstyle (Google Style, `validate` phase), SpotBugs (`verify` phase), ArchUnit (9 rules, runs with JUnit), Surefire (fail-fast at 1 failure)
#### Frontend Build & Test
- **Build:** `npm run build` (runs `generate:api` + parallel type-check and vite build)
- **Test:** `npm run test:unit` (Vitest)
- **Lint:** `npm run lint` (oxlint + ESLint)
- **Type check:** `vue-tsc --build`
- **Config:** `frontend/package.json` — Vue 3.5, Vite 7.3, TypeScript 5.9
- **Node engines:** `^20.19.0 || >=22.12.0`
### Scheduling and Parallelism
Per `spec/implementation-phases.md:15`, T-3 is at priority level 4* — parallelizable with T-4 (Development infrastructure). It can be implemented at any time now since T-1 and T-2 are done.
From `review-findings.md:105-107`: T-3 should be completed before the first user story is finished, otherwise code will exist without a pipeline.
### Gitea Actions Specifics
Gitea Actions uses the same YAML format as GitHub Actions with minor differences:
- Workflow files go in `.gitea/workflows/` (not `.github/workflows/`)
- Uses `runs-on: ubuntu-latest` or custom runner labels
- Container registry URL pattern: `{gitea-host}/{owner}/{repo}` (no separate registry domain)
- Supports `docker/login-action`, `docker/build-push-action`, and direct `docker` CLI
- Secrets are configured in the Gitea repository settings (e.g., `secrets.GITHUB_TOKEN` equivalent is typically `secrets.GITEA_TOKEN` or the built-in `gitea.token`)
### Build Commands for the Pipeline
The pipeline needs to execute these commands in order:
| Step | Command | Runs in |
|------|---------|---------|
| Backend test | `cd backend && ./mvnw -B verify` | JDK 25 environment |
| Frontend install | `cd frontend && npm ci` | Node 24 environment |
| Frontend lint | `cd frontend && npm run lint` | Node 24 environment |
| Frontend type-check | `cd frontend && npm run type-check` | Node 24 environment (needs OpenAPI spec) |
| Frontend test | `cd frontend && npm run test:unit -- --run` | Node 24 environment |
| Docker build | `docker build -t {registry}/{owner}/{repo}:{tag} .` | Docker-capable runner |
| Docker push | `docker push {registry}/{owner}/{repo}:{tag}` | Docker-capable runner |
Note: `npm run build` is implicitly tested by the Docker build stage (the Dockerfile runs `npm run build` in stage 1). Running it separately in CI is redundant but could provide faster feedback.
The `--run` flag on Vitest ensures it runs once and exits (non-watch mode).
### Container Registry
The Gitea container registry is built into Gitea. Docker images are pushed using the Gitea instance hostname as the registry. Authentication uses a Gitea API token or the built-in `GITHUB_TOKEN`-equivalent that Gitea Actions provides.
Push format: `docker push {gitea-host}/{owner}/{repo}:{tag}`
### What Does NOT Exist Yet
- No `.gitea/` directory
- No `.github/` directory
- No workflow YAML files anywhere
- No CI/CD configuration of any kind
- No Makefile or build orchestration script
- No documentation about the Gitea instance URL or registry configuration
## Code References
- `spec/setup-tasks.md:41-55` — T-3 specification and acceptance criteria
- `spec/implementation-phases.md:15` — T-3 scheduling (parallel with T-4)
- `.ralph/review-findings/questions.md:12-22` — Q-5 resolution (Gitea confirmed)
- `Dockerfile:1-26` — Multi-stage Docker build
- `Dockerfile:17` — Tests skipped in Docker build (deferred to CI)
- `.dockerignore:18-19``.gitea/` already excluded from Docker context
- `backend/pom.xml:56-168` — Maven build plugins (Checkstyle, Surefire, SpotBugs, OpenAPI generator)
- `frontend/package.json:6-17` — npm build and test scripts
- `CLAUDE.md` — Build commands reference table
## Architecture Documentation
### Pipeline Architecture Pattern
The project follows a "test in CI, skip in Docker" pattern:
- The Dockerfile is a pure build artifact — it produces a runnable image as fast as possible
- All quality gates (tests, linting, static analysis) are expected to run in the CI pipeline before the Docker build
- The Docker image is only published if all preceding steps pass
### Gitea Actions Workflow Pattern (GitHub Actions compatible)
Gitea Actions workflows follow the same `on/jobs/steps` YAML structure as GitHub Actions. The runner is a self-hosted instance with Docker available on the host, but the pipeline uses Buildah for container image builds to avoid Docker-in-Docker complexity.
### Image Tagging Strategy (resolved)
SemVer with rolling tags, following standard Docker convention. When a tag like `2.3.9` is pushed, the pipeline publishes the **same image** under all four tags:
| Tag | Type | Example | Updated when |
|-----|------|---------|-------------|
| `2.3.9` | Immutable | Exact version | Only once, on this release |
| `2.3` | Rolling | Latest `2.3.x` | Overwritten by any `2.3.x` release |
| `2` | Rolling | Latest `2.x.x` | Overwritten by any `2.x.x` release |
| `latest` | Rolling | Newest release | Overwritten by every release |
This means the pipeline does four pushes per release (one per tag). Users can pin `2` to get automatic minor and patch updates, or pin `2.3.9` for exact reproducibility.
Images are only published on tagged releases (Git tags matching a SemVer pattern), not on every push.
### Release Process (resolved)
Manual Git tags trigger releases. The workflow is:
```bash
git tag 1.2.3
git push --tags
```
The pipeline triggers on tags matching a SemVer pattern (e.g., `1.2.3`) and publishes the image with rolling SemVer tags. No `v` prefix — tags are pure SemVer. No automated versioning tools (release-please, semantic-release) — the developer decides the version.
### Container Build Tool (resolved)
Buildah is used instead of Docker for building and pushing container images. This avoids Docker-in-Docker issues entirely and works cleanly on self-hosted runners regardless of whether the runner process runs inside a container or on the host.
### Pipeline Quality Scope (resolved)
The pipeline runs the maximum quality gates available:
- **Backend:** `./mvnw verify` (full lifecycle — Checkstyle, JUnit 5, ArchUnit, SpotBugs)
- **Frontend:** Lint (`npm run lint`), type-check (`npm run type-check`), tests (`npm run test:unit -- --run`)
### Gitea Registry Authentication (researched)
Key findings from Gitea documentation and community:
- `GITHUB_TOKEN` / `GITEA_TOKEN` (the built-in runner token) does **not** have permissions to push to the Gitea container registry. It only has repository read access.
- A **Personal Access Token** (PAT) with package write permissions must be created and stored as a repository secret (e.g., `REGISTRY_TOKEN`).
- The registry URL is the Gitea instance hostname (e.g., `gitea.example.com`). `${{ github.server_url }}` provides it with protocol prefix — needs stripping or use a repository variable.
- Push URL format: `{registry-host}/{owner}/{repo}:{tag}`
### Reference Workflow (existing project)
An existing Gitea Actions workflow in the sibling project `../arr/.gitea/workflows/deploy.yaml` provides a reference:
- Runner label: `ubuntu-latest`
- Uses `${{ vars.DEPLOY_PATH }}` for repository variables
- Simple deploy pattern (git pull + docker compose up)
## Resolved Questions
1. **Gitea instance URL:** Configured via repository variable or derived from `${{ github.server_url }}`. The registry hostname is the same as the Gitea instance.
2. **Runner label:** `ubuntu-latest` (consistent with the existing `arr` project workflow).
3. **Runner setup:** Self-hosted runner on the host with Docker available. Buildah used for image builds to avoid DinD.
4. **Container build tool:** Buildah (no DinD needed).
5. **Image tagging strategy:** SemVer with rolling tags (`latest`, `2`, `2.3`, `2.3.9`). Published only on tagged releases.
6. **Branch protection / publish trigger:** Images are only published from tagged releases, not from branch pushes. Every push triggers test + build (without publish).
7. **Maven lifecycle scope:** `./mvnw verify` (full lifecycle including SpotBugs). Frontend also runs all available quality gates (lint, type-check, tests).
8. **Registry authentication:** Personal Access Token stored as repository secret (built-in `GITHUB_TOKEN` lacks package write permissions).
## Open Questions
None — all questions resolved.