Rename the app from tracker to fit
Some checks failed
Build and Deploy / build-and-deploy (push) Failing after 2m44s

The directory, the repo and the hostname are all fit; the module path,
the image, the namespace and the data file were still tracker. Nothing
is deployed yet, so this is free now and would not be later.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-01 22:03:33 -07:00
parent faf73c8be5
commit 3168a4ef96
14 changed files with 58 additions and 58 deletions

View File

@@ -1,5 +1,5 @@
# Where the log is written. In the cluster this is the mounted volume. # Where the log is written. In the cluster this is the mounted volume.
DATA_PATH=data/tracker.json DATA_PATH=data/fit.json
# Public origin, and the timezone the day rolls over in. # Public origin, and the timezone the day rolls over in.
BASE_URL=https://fit.scottyah.com BASE_URL=https://fit.scottyah.com

View File

@@ -30,12 +30,12 @@ jobs:
- name: Build image - name: Build image
run: | run: |
IMAGE=harbor.scottyah.com/scottyah/tracker IMAGE=harbor.scottyah.com/scottyah/fit
buildah --isolation chroot bud -t $IMAGE:${{ github.sha }} -t $IMAGE:latest . buildah --isolation chroot bud -t $IMAGE:${{ github.sha }} -t $IMAGE:latest .
- name: Push image - name: Push image
run: | run: |
IMAGE=harbor.scottyah.com/scottyah/tracker IMAGE=harbor.scottyah.com/scottyah/fit
# Harbor serves a publicly trusted certificate, so TLS is verified. # Harbor serves a publicly trusted certificate, so TLS is verified.
# Disabling verification would send these credentials, and accept the # Disabling verification would send these credentials, and accept the
# image, over a connection nothing has authenticated. # image, over a connection nothing has authenticated.
@@ -52,6 +52,6 @@ jobs:
chmod +x kubectl chmod +x kubectl
mkdir -p ~/.kube mkdir -p ~/.kube
echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config echo "${{ secrets.KUBECONFIG_DATA }}" | base64 -d > ~/.kube/config
sed -i "s|harbor.scottyah.com/scottyah/tracker:latest|harbor.scottyah.com/scottyah/tracker:${{ github.sha }}|" k8s.yaml sed -i "s|harbor.scottyah.com/scottyah/fit:latest|harbor.scottyah.com/scottyah/fit:${{ github.sha }}|" k8s.yaml
./kubectl apply -f k8s.yaml ./kubectl apply -f k8s.yaml
./kubectl rollout status deployment/tracker-dep -n tracker --timeout=120s ./kubectl rollout status deployment/fit-dep -n fit --timeout=120s

2
.gitignore vendored
View File

@@ -1,4 +1,4 @@
/tracker /fit
*.env *.env
.env .env
/data/ /data/

View File

@@ -14,15 +14,15 @@ COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \ RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \ -trimpath \
-ldflags="-s -w" \ -ldflags="-s -w" \
-o /out/tracker ./cmd/server -o /out/fit ./cmd/server
FROM scratch FROM scratch
COPY --from=build /out/tracker /tracker COPY --from=build /out/fit /fit
# nobody:nogroup — the only writable path is the mounted volume at /data. # nobody:nogroup — the only writable path is the mounted volume at /data.
USER 65534:65534 USER 65534:65534
EXPOSE 8080 EXPOSE 8080
ENTRYPOINT ["/tracker"] ENTRYPOINT ["/fit"]

View File

@@ -1,4 +1,4 @@
# tracker # fit
A 75-day challenge, five things a day: the workout, the protein, the calories, A 75-day challenge, five things a day: the workout, the protein, the calories,
the water, and three lines of gratitude. the water, and three lines of gratitude.
@@ -40,7 +40,7 @@ faces for everything else, so there are no web fonts to serve.
Needs Go 1.26. No database. Needs Go 1.26. No database.
```sh ```sh
export DATA_PATH="$PWD/data/tracker.json" export DATA_PATH="$PWD/data/fit.json"
export TZ_NAME="America/Los_Angeles" export TZ_NAME="America/Los_Angeles"
go run ./cmd/server go run ./cmd/server
@@ -57,7 +57,7 @@ go test ./...
| Variable | Required | Notes | | Variable | Required | Notes |
| --- | --- | --- | | --- | --- | --- |
| `DATA_PATH` | no | The log file. Default `data/tracker.json`, relative to the working directory. The directory is created if missing. | | `DATA_PATH` | no | The log file. Default `data/fit.json`, relative to the working directory. The directory is created if missing. |
| `TZ_NAME` | no | Default `America/Los_Angeles`. Decides when the day rolls over; a wrong value files entries against the wrong date. | | `TZ_NAME` | no | Default `America/Los_Angeles`. Decides when the day rolls over; a wrong value files entries against the wrong date. |
| `BASE_URL` | no | Public origin. Default `http://localhost:8080`. | | `BASE_URL` | no | Public origin. Default `http://localhost:8080`. |
| `ADDR` | no | Listen address, default `:8080`. | | `ADDR` | no | Listen address, default `:8080`. |
@@ -76,8 +76,8 @@ guarding the log, so pick a real password:
```sh ```sh
htpasswd -nbB scott 'a-real-password' > /tmp/users htpasswd -nbB scott 'a-real-password' > /tmp/users
kubectl create namespace tracker kubectl create namespace fit
kubectl create secret generic tracker-basic-auth -n tracker --from-file=users=/tmp/users kubectl create secret generic fit-basic-auth -n fit --from-file=users=/tmp/users
rm /tmp/users rm /tmp/users
``` ```
@@ -87,7 +87,7 @@ existing `scottyah-tls` secret.
To pull a copy of the log without the browser: To pull a copy of the log without the browser:
```sh ```sh
kubectl exec -n tracker deploy/tracker-dep -- cat /data/tracker.json > tracker.json kubectl exec -n fit deploy/fit-dep -- cat /data/fit.json > fit.json
``` ```
## Notes on how it's built ## Notes on how it's built

View File

@@ -14,9 +14,9 @@ import (
// correctly inside a scratch image, which has no /usr/share/zoneinfo. // correctly inside a scratch image, which has no /usr/share/zoneinfo.
_ "time/tzdata" _ "time/tzdata"
"github.com/scottyah/tracker/internal/config" "github.com/scottyah/fit/internal/config"
"github.com/scottyah/tracker/internal/store" "github.com/scottyah/fit/internal/store"
"github.com/scottyah/tracker/internal/web" "github.com/scottyah/fit/internal/web"
) )
func main() { func main() {

2
go.mod
View File

@@ -1,3 +1,3 @@
module github.com/scottyah/tracker module github.com/scottyah/fit
go 1.26.1 go 1.26.1

View File

@@ -24,7 +24,7 @@ func Load() (*Config, error) {
c := &Config{ c := &Config{
Addr: env("ADDR", ":8080"), Addr: env("ADDR", ":8080"),
BaseURL: strings.TrimRight(env("BASE_URL", "http://localhost:8080"), "/"), BaseURL: strings.TrimRight(env("BASE_URL", "http://localhost:8080"), "/"),
DataPath: env("DATA_PATH", "data/tracker.json"), DataPath: env("DATA_PATH", "data/fit.json"),
} }
// The whole app is date arithmetic, so a wrong timezone silently logs // The whole app is date arithmetic, so a wrong timezone silently logs

View File

@@ -284,7 +284,7 @@ func (s *Store) Export() ([]byte, error) {
func (s *Store) Import(body []byte) error { func (s *Store) Import(body []byte) error {
var next data var next data
if err := json.Unmarshal(body, &next); err != nil { if err := json.Unmarshal(body, &next); err != nil {
return fmt.Errorf("that file is not a tracker export: %w", err) return fmt.Errorf("that file is not a fit export: %w", err)
} }
if next.Days == nil { if next.Days == nil {
next.Days = map[string]*Day{} next.Days = map[string]*Day{}
@@ -313,7 +313,7 @@ func (s *Store) persist() error {
} }
dir := filepath.Dir(s.path) dir := filepath.Dir(s.path)
tmp, err := os.CreateTemp(dir, ".tracker-*.json") tmp, err := os.CreateTemp(dir, ".fit-*.json")
if err != nil { if err != nil {
return fmt.Errorf("create temp file: %w", err) return fmt.Errorf("create temp file: %w", err)
} }

View File

@@ -96,7 +96,7 @@ func TestMarks(t *testing.T) {
} }
func TestSaveAndReopen(t *testing.T) { func TestSaveAndReopen(t *testing.T) {
path := filepath.Join(t.TempDir(), "nested", "tracker.json") path := filepath.Join(t.TempDir(), "nested", "fit.json")
st, err := Open(path) st, err := Open(path)
if err != nil { if err != nil {
@@ -128,7 +128,7 @@ func TestSaveAndReopen(t *testing.T) {
} }
func TestSaveEmptyDayRemovesIt(t *testing.T) { func TestSaveEmptyDayRemovesIt(t *testing.T) {
path := filepath.Join(t.TempDir(), "tracker.json") path := filepath.Join(t.TempDir(), "fit.json")
st, _ := Open(path) st, _ := Open(path)
if err := st.SaveDay(Day{Date: "2026-07-01", Workout: true}); err != nil { if err := st.SaveDay(Day{Date: "2026-07-01", Workout: true}); err != nil {
@@ -145,7 +145,7 @@ func TestSaveEmptyDayRemovesIt(t *testing.T) {
// A day handed out must not share memory with the stored one, or editing the // A day handed out must not share memory with the stored one, or editing the
// copy would quietly rewrite history. // copy would quietly rewrite history.
func TestDayIsACopy(t *testing.T) { func TestDayIsACopy(t *testing.T) {
path := filepath.Join(t.TempDir(), "tracker.json") path := filepath.Join(t.TempDir(), "fit.json")
st, _ := Open(path) st, _ := Open(path)
st.SaveDay(Day{Date: "2026-07-01", Protein: ptr(190)}) st.SaveDay(Day{Date: "2026-07-01", Protein: ptr(190)})
@@ -158,7 +158,7 @@ func TestDayIsACopy(t *testing.T) {
} }
func TestOpenRefusesUnreadableFile(t *testing.T) { func TestOpenRefusesUnreadableFile(t *testing.T) {
path := filepath.Join(t.TempDir(), "tracker.json") path := filepath.Join(t.TempDir(), "fit.json")
if err := os.WriteFile(path, []byte("{not json"), 0o644); err != nil { if err := os.WriteFile(path, []byte("{not json"), 0o644); err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -168,7 +168,7 @@ func TestOpenRefusesUnreadableFile(t *testing.T) {
} }
func TestDaysFillsGaps(t *testing.T) { func TestDaysFillsGaps(t *testing.T) {
path := filepath.Join(t.TempDir(), "tracker.json") path := filepath.Join(t.TempDir(), "fit.json")
st, _ := Open(path) st, _ := Open(path)
st.SaveSettings(settings()) st.SaveSettings(settings())
st.SaveDay(Day{Date: "2026-06-22", Workout: true}) st.SaveDay(Day{Date: "2026-06-22", Workout: true})
@@ -189,7 +189,7 @@ func TestDaysFillsGaps(t *testing.T) {
} }
func TestImportRejectsJunkDates(t *testing.T) { func TestImportRejectsJunkDates(t *testing.T) {
path := filepath.Join(t.TempDir(), "tracker.json") path := filepath.Join(t.TempDir(), "fit.json")
st, _ := Open(path) st, _ := Open(path)
if err := st.Import([]byte(`{"days":{"last tuesday":{"workout":true}}}`)); err == nil { if err := st.Import([]byte(`{"days":{"last tuesday":{"workout":true}}}`)); err == nil {
t.Fatal("Import accepted a key that is not a date") t.Fatal("Import accepted a key that is not a date")

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"time" "time"
"github.com/scottyah/tracker/internal/store" "github.com/scottyah/fit/internal/store"
) )
// gratitudeMax caps one gratitude line. It is a line, not an essay, and a cap // gratitudeMax caps one gratitude line. It is a line, not an essay, and a cap
@@ -250,7 +250,7 @@ func (s *Server) handleExport(w http.ResponseWriter, r *http.Request) {
s.fail(w, http.StatusInternalServerError, "The export did not build: "+err.Error()) s.fail(w, http.StatusInternalServerError, "The export did not build: "+err.Error())
return return
} }
name := "tracker-" + s.today() + ".json" name := "fit-" + s.today() + ".json"
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`) w.Header().Set("Content-Disposition", `attachment; filename="`+name+`"`)
w.Write(body) w.Write(body)

View File

@@ -10,8 +10,8 @@ import (
"net/http" "net/http"
"time" "time"
"github.com/scottyah/tracker/internal/config" "github.com/scottyah/fit/internal/config"
"github.com/scottyah/tracker/internal/store" "github.com/scottyah/fit/internal/store"
) )
//go:embed templates/*.html //go:embed templates/*.html

View File

@@ -9,8 +9,8 @@ import (
"testing" "testing"
"time" "time"
"github.com/scottyah/tracker/internal/config" "github.com/scottyah/fit/internal/config"
"github.com/scottyah/tracker/internal/store" "github.com/scottyah/fit/internal/store"
) )
// newServer builds a server whose "today" is fixed, so the day arithmetic is // newServer builds a server whose "today" is fixed, so the day arithmetic is
@@ -22,7 +22,7 @@ func newServer(t *testing.T, today string) (*Server, *store.Store) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
st, err := store.Open(filepath.Join(t.TempDir(), "tracker.json")) st, err := store.Open(filepath.Join(t.TempDir(), "fit.json"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
@@ -240,11 +240,11 @@ func TestExportThenImport(t *testing.T) {
if rec.Code != http.StatusOK { if rec.Code != http.StatusOK {
t.Fatalf("status %d", rec.Code) t.Fatalf("status %d", rec.Code)
} }
if !strings.Contains(rec.Header().Get("Content-Disposition"), "tracker-2026-07-31.json") { if !strings.Contains(rec.Header().Get("Content-Disposition"), "fit-2026-07-31.json") {
t.Errorf("download name = %q", rec.Header().Get("Content-Disposition")) t.Errorf("download name = %q", rec.Header().Get("Content-Disposition"))
} }
fresh, err := store.Open(filepath.Join(t.TempDir(), "tracker.json")) fresh, err := store.Open(filepath.Join(t.TempDir(), "fit.json"))
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }

View File

@@ -2,7 +2,7 @@
apiVersion: v1 apiVersion: v1
kind: Namespace kind: Namespace
metadata: metadata:
name: tracker name: fit
--- ---
# The whole log is one JSON file, so the "database" is this volume. It is # The whole log is one JSON file, so the "database" is this volume. It is
@@ -10,8 +10,8 @@ metadata:
apiVersion: v1 apiVersion: v1
kind: PersistentVolumeClaim kind: PersistentVolumeClaim
metadata: metadata:
name: tracker-data-pvc name: fit-data-pvc
namespace: tracker namespace: fit
spec: spec:
accessModes: accessModes:
- ReadWriteOnce - ReadWriteOnce
@@ -23,11 +23,11 @@ spec:
apiVersion: v1 apiVersion: v1
kind: Service kind: Service
metadata: metadata:
name: tracker-svc name: fit-svc
namespace: tracker namespace: fit
spec: spec:
selector: selector:
app: tracker app: fit
ports: ports:
- port: 80 - port: 80
targetPort: 8080 targetPort: 8080
@@ -37,8 +37,8 @@ spec:
apiVersion: apps/v1 apiVersion: apps/v1
kind: Deployment kind: Deployment
metadata: metadata:
name: tracker-dep name: fit-dep
namespace: tracker namespace: fit
spec: spec:
replicas: 1 replicas: 1
# Two pods would both hold the file in memory and overwrite each other's # Two pods would both hold the file in memory and overwrite each other's
@@ -48,11 +48,11 @@ spec:
type: Recreate type: Recreate
selector: selector:
matchLabels: matchLabels:
app: tracker app: fit
template: template:
metadata: metadata:
labels: labels:
app: tracker app: fit
spec: spec:
imagePullSecrets: imagePullSecrets:
- name: harborcred - name: harborcred
@@ -64,8 +64,8 @@ spec:
# write the file it exists to hold. # write the file it exists to hold.
fsGroup: 65534 fsGroup: 65534
containers: containers:
- name: tracker - name: fit
image: harbor.scottyah.com/scottyah/tracker:latest image: harbor.scottyah.com/scottyah/fit:latest
imagePullPolicy: Always imagePullPolicy: Always
ports: ports:
- containerPort: 8080 - containerPort: 8080
@@ -75,7 +75,7 @@ spec:
- name: BASE_URL - name: BASE_URL
value: "https://fit.scottyah.com" value: "https://fit.scottyah.com"
- name: DATA_PATH - name: DATA_PATH
value: "/data/tracker.json" value: "/data/fit.json"
# Every date in the app is a local date, so this decides when the # Every date in the app is a local date, so this decides when the
# day rolls over. # day rolls over.
- name: TZ_NAME - name: TZ_NAME
@@ -113,7 +113,7 @@ spec:
volumes: volumes:
- name: data - name: data
persistentVolumeClaim: persistentVolumeClaim:
claimName: tracker-data-pvc claimName: fit-data-pvc
--- ---
# The app has no login of its own; this is the only thing between the log and # The app has no login of its own; this is the only thing between the log and
@@ -121,22 +121,22 @@ spec:
apiVersion: traefik.io/v1alpha1 apiVersion: traefik.io/v1alpha1
kind: Middleware kind: Middleware
metadata: metadata:
name: tracker-basic-auth name: fit-basic-auth
namespace: tracker namespace: fit
spec: spec:
basicAuth: basicAuth:
secret: tracker-basic-auth secret: fit-basic-auth
--- ---
apiVersion: networking.k8s.io/v1 apiVersion: networking.k8s.io/v1
kind: Ingress kind: Ingress
metadata: metadata:
name: tracker-ingress name: fit-ingress
namespace: tracker namespace: fit
annotations: annotations:
traefik.ingress.kubernetes.io/router.entrypoints: websecure traefik.ingress.kubernetes.io/router.entrypoints: websecure
traefik.ingress.kubernetes.io/router.tls: "true" traefik.ingress.kubernetes.io/router.tls: "true"
traefik.ingress.kubernetes.io/router.middlewares: tracker-tracker-basic-auth@kubernetescrd traefik.ingress.kubernetes.io/router.middlewares: fit-fit-basic-auth@kubernetescrd
spec: spec:
ingressClassName: traefik ingressClassName: traefik
tls: tls:
@@ -151,6 +151,6 @@ spec:
pathType: Prefix pathType: Prefix
backend: backend:
service: service:
name: tracker-svc name: fit-svc
port: port:
number: 80 number: 80