Add container publishing and env-based runtime
CI / test (push) Successful in 30s
CI / publish (push) Failing after 27s

This commit is contained in:
Joe Julian
2026-03-28 15:36:52 -07:00
parent 36af73a209
commit 5f16410960
5 changed files with 103 additions and 53 deletions
+30
View File
@@ -23,3 +23,33 @@ jobs:
- name: Build - name: Build
run: go build . run: go build .
publish:
if: gitea.event_name == 'push' && gitea.ref == 'refs/heads/main'
needs: test
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install Docker CLI
run: |
apt-get update
apt-get install -y docker.io
- name: Login to Gitea Registry
env:
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
run: |
registry_host="${GITHUB_SERVER_URL#http://}"
registry_host="${registry_host#https://}"
printf '%s' "${REGISTRY_PASSWORD}" | docker login "${registry_host}" -u "${GITHUB_REPOSITORY_OWNER}" --password-stdin
- name: Build and Push Image
run: |
registry_host="${GITHUB_SERVER_URL#http://}"
registry_host="${registry_host#https://}"
image="${registry_host}/${GITHUB_REPOSITORY}"
docker build -t "${image}:main" -t "${image}:sha-${GITHUB_SHA}" .
docker push "${image}:main"
docker push "${image}:sha-${GITHUB_SHA}"
+10 -4
View File
@@ -12,11 +12,13 @@ Required when `SEND_EMAIL=true`:
Recommended settings: Recommended settings:
- `ALMA_CREDS_FILE=/config/alma.creds`
- `ALMA_ASSIGNMENTS_URL=https://example.invalid/children/student-id/assignments` - `ALMA_ASSIGNMENTS_URL=https://example.invalid/children/student-id/assignments`
- `ALMA_SCHEDULE_URL=https://example.invalid/children/student-id/schedule` - `ALMA_SCHEDULE_URL=https://example.invalid/children/student-id/schedule`
- `ALMA_START_DATE=2026-01-20` - `ALMA_START_DATE=2026-01-20`
- `ALMA_UPCOMING_DAYS=14` - `ALMA_UPCOMING_DAYS=14`
- `ALMA_CREDS_FILE=/config/alma.creds`
- `ALMA_USERNAME`
- `ALMA_PASSWORD`
- `SMTP_PORT=587` - `SMTP_PORT=587`
- `SMTP_STARTTLS=true` - `SMTP_STARTTLS=true`
- `SMTP_USERNAME` - `SMTP_USERNAME`
@@ -26,7 +28,10 @@ Recommended settings:
Any setting can also be supplied via a `*_FILE` variant such as `SMTP_PASSWORD_FILE`. Any setting can also be supplied via a `*_FILE` variant such as `SMTP_PASSWORD_FILE`.
The Alma credentials file format is: The Alma credentials can be supplied either by:
- `ALMA_USERNAME` and `ALMA_PASSWORD`
- `ALMA_CREDS_FILE` containing:
```yaml ```yaml
username: your-alma-username username: your-alma-username
@@ -48,8 +53,9 @@ It runs on pushes to `main` and pull requests, and currently:
- runs `go test ./...` - runs `go test ./...`
- runs `go build .` - runs `go build .`
- builds and pushes `:main` and `:sha-<commit>` container tags on pushes to `main`
The workflow expects a runner with the `ubuntu-latest` label. The cluster runner deployed for this repo provides that label. The workflow expects a runner with the `ubuntu-latest` label and a repository Actions secret named `REGISTRY_PASSWORD` that can push to the Gitea container registry. The cluster runner deployed for this repo provides the required runner label.
## Container ## Container
@@ -63,4 +69,4 @@ The container image uses a static Go binary in `scratch`, with only the CA bundl
## Kubernetes ## Kubernetes
Use a Secret for Alma and SMTP credentials. The example manifest in `cronjob.example.yaml` mounts Alma credentials at `/config/alma.creds` and reads SMTP credentials from secret-backed environment variables. Use a Secret for Alma and SMTP credentials. The example manifest in `cronjob.example.yaml` reads all runtime settings from Kubernetes secrets and does not require a credentials file mount.
+9 -41
View File
@@ -2,53 +2,21 @@ apiVersion: batch/v1
kind: CronJob kind: CronJob
metadata: metadata:
name: alma-assignments-reporter name: alma-assignments-reporter
namespace: default namespace: email
spec: spec:
timeZone: America/Los_Angeles
schedule: "0 14 * * *" schedule: "0 14 * * *"
jobTemplate: jobTemplate:
spec: spec:
template: template:
spec: spec:
imagePullSecrets:
- name: alma-assignments-reporter-registry
restartPolicy: Never restartPolicy: Never
containers: containers:
- name: reporter - name: reporter
image: example.invalid/alma-assignments-reporter:latest image: example.invalid/alma-assignments-reporter:main
env: imagePullPolicy: Always
- name: ALMA_ASSIGNMENTS_URL envFrom:
value: https://example.invalid/children/student-id/assignments - secretRef:
- name: ALMA_SCHEDULE_URL name: alma-assignments-reporter
value: https://example.invalid/children/student-id/schedule
- name: ALMA_START_DATE
value: "2026-01-20"
- name: ALMA_UPCOMING_DAYS
value: "14"
- name: ALMA_CREDS_FILE
value: /config/alma.creds
- name: SMTP_HOST
value: smtp.email.svc.cluster.local
- name: SMTP_PORT
value: "587"
- name: SMTP_STARTTLS
value: "false"
- name: EMAIL_FROM
value: alma-reporter@example.invalid
- name: EMAIL_TO
value: parent@example.invalid
- name: SMTP_USERNAME
valueFrom:
secretKeyRef:
name: alma-assignments-reporter-smtp
key: username
- name: SMTP_PASSWORD
valueFrom:
secretKeyRef:
name: alma-assignments-reporter-smtp
key: password
volumeMounts:
- name: alma-creds
mountPath: /config
readOnly: true
volumes:
- name: alma-creds
secret:
secretName: alma-assignments-reporter-alma
+15 -8
View File
@@ -147,7 +147,7 @@ func loadConfig() (Config, error) {
if err != nil { if err != nil {
return Config{}, fmt.Errorf("parse ALMA_UPCOMING_DAYS: %w", err) return Config{}, fmt.Errorf("parse ALMA_UPCOMING_DAYS: %w", err)
} }
almaCredsFile, err := readValue("ALMA_CREDS_FILE", "/config/alma.creds", true) almaCredsFile, err := readValue("ALMA_CREDS_FILE", "/config/alma.creds", false)
if err != nil { if err != nil {
return Config{}, err return Config{}, err
} }
@@ -269,6 +269,20 @@ func splitCSV(value string) []string {
} }
func loadAlmaCreds(filePath string) (map[string]string, error) { func loadAlmaCreds(filePath string) (map[string]string, error) {
if envUser, _ := readValue("ALMA_USERNAME", "", false); envUser != "" {
if envPass, _ := readValue("ALMA_PASSWORD", "", false); envPass != "" {
return map[string]string{
"username": envUser,
"password": envPass,
}, nil
}
return nil, errors.New("ALMA_PASSWORD is required when ALMA_USERNAME is set")
}
if filePath == "" {
return nil, errors.New("missing Alma credentials: set ALMA_USERNAME/ALMA_PASSWORD or ALMA_CREDS_FILE")
}
data, err := os.ReadFile(filePath) data, err := os.ReadFile(filePath)
if err != nil { if err != nil {
return nil, fmt.Errorf("read alma creds: %w", err) return nil, fmt.Errorf("read alma creds: %w", err)
@@ -291,13 +305,6 @@ func loadAlmaCreds(filePath string) (map[string]string, error) {
return nil, err return nil, err
} }
if envUser, _ := readValue("ALMA_USERNAME", "", false); envUser != "" {
values["username"] = envUser
}
if envPass, _ := readValue("ALMA_PASSWORD", "", false); envPass != "" {
values["password"] = envPass
}
if values["username"] == "" || values["password"] == "" { if values["username"] == "" || values["password"] == "" {
return nil, errors.New("alma creds file must contain username and password") return nil, errors.New("alma creds file must contain username and password")
} }
+39
View File
@@ -1,6 +1,7 @@
package main package main
import ( import (
"os"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -93,6 +94,44 @@ func TestExtractLikeLiveMissingTable(t *testing.T) {
} }
} }
func TestLoadAlmaCredsFromEnv(t *testing.T) {
t.Setenv("ALMA_USERNAME", "student")
t.Setenv("ALMA_PASSWORD", "secret")
got, err := loadAlmaCreds("")
if err != nil {
t.Fatalf("loadAlmaCreds returned error: %v", err)
}
if got["username"] != "student" || got["password"] != "secret" {
t.Fatalf("unexpected creds: %#v", got)
}
}
func TestLoadAlmaCredsRequiresPasswordWithEnvUsername(t *testing.T) {
t.Setenv("ALMA_USERNAME", "student")
t.Setenv("ALMA_PASSWORD", "")
if _, err := loadAlmaCreds(""); err == nil {
t.Fatal("expected error when ALMA_PASSWORD is missing")
}
}
func TestLoadAlmaCredsFromFile(t *testing.T) {
dir := t.TempDir()
path := dir + "/alma.creds"
if err := os.WriteFile(path, []byte("username: student\npassword: secret\n"), 0o600); err != nil {
t.Fatalf("write creds file: %v", err)
}
got, err := loadAlmaCreds(path)
if err != nil {
t.Fatalf("loadAlmaCreds returned error: %v", err)
}
if got["username"] != "student" || got["password"] != "secret" {
t.Fatalf("unexpected creds: %#v", got)
}
}
func mustParseHTML(text string) *html.Node { func mustParseHTML(text string) *html.Node {
doc, err := html.Parse(strings.NewReader(text)) doc, err := html.Parse(strings.NewReader(text))
if err != nil { if err != nil {