Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
18 commits
Select commit Hold shift + click to select a range
22f92d1
docs: add PR template
markovav-official Jun 4, 2026
7d42a4c
feat(lab6): Dockerize QuickNotes with distroless image and compose stack
markovav-official Jun 23, 2026
b2295cb
feat(lab8): Prometheus, Grafana golden signals, and error-rate alert
markovav-official Jun 30, 2026
577dc11
feat(lab9): Trivy/ZAP scans, security headers middleware, govulncheck CI
markovav-official Jul 7, 2026
0a943fc
fix(lab9): run govulncheck on Go 1.25.11 to clear stdlib CVEs
markovav-official Jul 7, 2026
91afcb9
docs(lab9): add govulncheck red/green CI links and screenshots
markovav-official Jul 7, 2026
8354a44
docs(lab10): add submission template for deadline MR
markovav-official Jul 7, 2026
f989754
feat(lab10): ghcr release workflow, HF Space config, cloud tunnel scr…
markovav-official Jul 7, 2026
7a05f2e
fix(lab10): lowercase ghcr image name for registry rules
markovav-official Jul 7, 2026
11c4b26
docs(lab10): add Task 1 release run and docker pull evidence
markovav-official Jul 7, 2026
e874eb2
docs(lab10): HF Space, tunnel bonus, and latency evidence
markovav-official Jul 7, 2026
b2af60b
feat(lab11): Nix flake for reproducible QuickNotes build and OCI image
markovav-official Jul 8, 2026
733ccbf
chore(lab11): break nix-repro job A for red CI demo
markovav-official Jul 8, 2026
3e4dd6f
fix(lab11): wire SOURCE_DATE_EPOCH into flake for red CI demo
markovav-official Jul 8, 2026
9c692be
fix(lab11): restore nix-repro parity after red CI demo
markovav-official Jul 8, 2026
5f41ec7
docs(lab11): finalize submission with green/red CI run URLs
markovav-official Jul 8, 2026
545a17c
fix(ci): bump govulncheck Go toolchain to 1.25.12
markovav-official Jul 8, 2026
e652e44
feat(lab12): Spin WASM /time endpoint with perf comparison and wasmti…
markovav-official Jul 8, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
## Goal
<!-- What does this PR accomplish? 1 sentence. -->

## Changes
-

## Testing
<!-- How did you verify it? -->

## Checklist
- [ ] Title is a clear sentence (≤ 70 chars)
- [ ] Commits are signed (`git log --show-signature`)
- [ ] `submissions/labN.md` updated
128 changes: 128 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
name: ci

on:
push:
branches: [main]
paths:
- 'app/**'
- '.github/workflows/**'
pull_request:
branches: [main]
paths:
- 'app/**'
- '.github/workflows/**'

permissions:
contents: read

env:
GOFLAGS: -buildvcs=false

jobs:
changes:
name: changes
runs-on: ubuntu-24.04
outputs:
app-go: ${{ steps.filter.outputs.app-go }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
- uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3.0.2
id: filter
with:
filters: |
app-go:
- 'app/**/*.go'
- 'app/go.mod'
- 'app/go.sum'
- 'app/.golangci.yml'

vet:
name: vet
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go: ['1.23', '1.24']
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
with:
fetch-depth: 1
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: ${{ matrix.go }}
cache: true
cache-dependency-path: app/go.mod
- run: go vet ./...

test:
name: test
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
go: ['1.23', '1.24']
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
with:
fetch-depth: 1
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: ${{ matrix.go }}
cache: true
cache-dependency-path: app/go.mod
- run: go test -race -count=1 ./...

lint:
name: lint
needs: changes
if: needs.changes.outputs.app-go == 'true'
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
with:
fetch-depth: 1
- uses: actions/cache@d4323d4df104b026a6aa633fdb11d772146be0bf # v4.2.2
with:
path: |
~/.cache/golangci-lint
~/go/bin
key: golangci-lint-${{ runner.os }}-v2.5.0-${{ hashFiles('app/.golangci.yml') }}
restore-keys: |
golangci-lint-${{ runner.os }}-v2.5.0-
- uses: golangci/golangci-lint-action@1481404843c368bc19ca9406f87d6e0fc97bdcfd # v7.0.0
with:
version: v2.5.0
working-directory: app

govulncheck:
name: govulncheck
runs-on: ubuntu-24.04
defaults:
run:
working-directory: app
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2
with:
fetch-depth: 1
- uses: actions/setup-go@0aaccfd150d50ccaeb58ebd88d36e91967a5f35b # v5.4.0
with:
go-version: '1.25.12'
cache: true
cache-dependency-path: app/go.mod
- run: go install golang.org/x/vuln/cmd/govulncheck@v1.1.4
- run: govulncheck ./...

ci-ok:
name: ci-ok
if: always()
needs: [vet, test, lint, govulncheck]
runs-on: ubuntu-24.04
steps:
- run: |
test "${{ contains(needs.*.result, 'failure') || contains(needs.*.result, 'cancelled') }}" = "false"
53 changes: 53 additions & 0 deletions .github/workflows/nix-repro.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
name: nix-repro

on:
push:
branches: ['**']
pull_request:
branches: [main]

permissions:
contents: read

jobs:
build-a:
name: nix-build-a
runs-on: ubuntu-24.04
outputs:
digest: ${{ steps.hash.outputs.digest }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2

- uses: DeterminateSystems/nix-installer-action@90bb610b90bf290cad97484ba341453bd1cbefea # v19

- id: build
run: nix build .#docker --print-build-logs --print-out-paths > out-path.txt

- id: hash
run: echo "digest=$(sha256sum "$(cat out-path.txt)" | awk '{print $1}')" >> "$GITHUB_OUTPUT"

build-b:
name: nix-build-b
runs-on: ubuntu-24.04
outputs:
digest: ${{ steps.hash.outputs.digest }}
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2

- uses: DeterminateSystems/nix-installer-action@90bb610b90bf290cad97484ba341453bd1cbefea # v19

- id: build
run: nix build .#docker --print-build-logs --print-out-paths > out-path.txt

- id: hash
run: echo "digest=$(sha256sum "$(cat out-path.txt)" | awk '{print $1}')" >> "$GITHUB_OUTPUT"

compare:
name: nix-repro-ok
needs: [build-a, build-b]
runs-on: ubuntu-24.04
steps:
- run: |
echo "build-a: ${{ needs.build-a.outputs.digest }}"
echo "build-b: ${{ needs.build-b.outputs.digest }}"
test "${{ needs.build-a.outputs.digest }}" = "${{ needs.build-b.outputs.digest }}"
43 changes: 43 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
name: release

on:
push:
tags:
- 'v*'

permissions:
contents: read
packages: write

env:
REGISTRY: ghcr.io

jobs:
release:
name: release
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4.2.2

- id: image
run: |
repo="$(echo '${{ github.repository }}' | tr '[:upper:]' '[:lower:]')"
echo "name=ghcr.io/${repo}/quicknotes" >> "$GITHUB_OUTPUT"

- uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0

- uses: docker/login-action@74a5d142397b4f367a81961eba4e8cd7edddf772 # v3.4.0
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}

- uses: docker/build-push-action@263435318d21b8e681c14492fe198d362a7d2c83 # v6.18.0
with:
context: app
file: app/Dockerfile
platforms: linux/amd64
push: true
tags: |
${{ steps.image.outputs.name }}:${{ github.ref_name }}
${{ steps.image.outputs.name }}:latest
21 changes: 21 additions & 0 deletions app/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# syntax=docker/dockerfile:1

FROM golang:1.24-alpine AS builder
WORKDIR /src
COPY go.mod ./
RUN go mod download
COPY . .
RUN CGO_ENABLED=0 GOOS=linux go build \
-trimpath \
-ldflags='-s -w' \
-o /quicknotes .

FROM busybox:1.36-musl AS busybox

FROM gcr.io/distroless/static-debian12:nonroot
COPY --from=builder /quicknotes /quicknotes
COPY --from=builder /src/seed.json /seed.json
COPY --from=busybox /bin/busybox /busybox
USER nonroot:nonroot
EXPOSE 8080
ENTRYPOINT ["/quicknotes"]
4 changes: 4 additions & 0 deletions app/handlers.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@ func NewServer(store *Store) *Server {
return &Server{store: store, requestsByCode: by}
}

func (s *Server) Handler() http.Handler {
return securityHeaders(s.Routes())
}

func (s *Server) Routes() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("GET /health", s.wrap(s.handleHealth))
Expand Down
25 changes: 24 additions & 1 deletion app/handlers_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ func do(t *testing.T, srv *Server, method, target string, body any) *httptest.Re
}
req := httptest.NewRequest(method, target, &buf)
rec := httptest.NewRecorder()
srv.Routes().ServeHTTP(rec, req)
srv.Handler().ServeHTTP(rec, req)
return rec
}

Expand Down Expand Up @@ -131,3 +131,26 @@ func TestMetrics_ExposesPrometheusFormat(t *testing.T) {
}
}

func TestSecurityHeaders_PresentOnAllRoutes(t *testing.T) {
srv := newTestServer(t)
rec := do(t, srv, http.MethodGet, "/health", nil)
for _, key := range []string{
"X-Content-Type-Options",
"X-Frame-Options",
"Content-Security-Policy",
"Referrer-Policy",
"Permissions-Policy",
"Cache-Control",
} {
if got := rec.Header().Get(key); got == "" {
t.Errorf("missing %s header", key)
}
}
if got := rec.Header().Get("X-Frame-Options"); got != "DENY" {
t.Errorf("X-Frame-Options: got %q want DENY", got)
}
if got := rec.Header().Get("Content-Security-Policy"); got != "default-src 'none'" {
t.Errorf("CSP: got %q want default-src 'none'", got)
}
}

2 changes: 1 addition & 1 deletion app/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ func main() {
server := NewServer(store)
srv := &http.Server{
Addr: addr,
Handler: server.Routes(),
Handler: server.Handler(),
ReadHeaderTimeout: 5 * time.Second,
}

Expand Down
18 changes: 18 additions & 0 deletions app/security.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package main

import "net/http"

// securityHeaders applies baseline HTTP security headers to every response.
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
h := w.Header()
h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-Frame-Options", "DENY")
h.Set("Content-Security-Policy", "default-src 'none'")
h.Set("Referrer-Policy", "no-referrer")
h.Set("Permissions-Policy", "geolocation=(), microphone=(), camera=()")
h.Set("Cache-Control", "no-cache, no-store, must-revalidate, private")
h.Set("Pragma", "no-cache")
next.ServeHTTP(w, r)
})
}
4 changes: 4 additions & 0 deletions cloud/hf-space/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
# Pull the CI-built image (linux/amd64) instead of rebuilding in HF.
# Trade-off: faster Space builds and identical artifact to ghcr.io release;
# requires public package visibility on first push.
FROM ghcr.io/markovav-official/devops-intro/quicknotes:v0.1.0
19 changes: 19 additions & 0 deletions cloud/hf-space/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
---
title: QuickNotes Lab 10
emoji: 📝
colorFrom: blue
colorTo: green
sdk: docker
app_port: 8080
pinned: false
license: mit
---

# QuickNotes on Hugging Face Spaces

DevOps Intro Lab 10 — same container image as [`ghcr.io/markovav-official/devops-intro/quicknotes`](https://github.com/markovav-official/DevOps-Intro/pkgs/container/devops-intro%2Fquicknotes).

- `/health` — liveness
- `/notes` — JSON API

Built from the course fork; image pushed by `.github/workflows/release.yml` on `v*` tags.
Loading