diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e17ee37 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,7 @@ +.git +.gitea +.gitignore +Dockerfile +.dockerignore +README.md +*.csv diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..ec07b3b --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,57 @@ +name: Build + +# Builds the image on every push to main and pushes it to Gitea's container +# registry as "latest" (after first re-tagging the current "latest" as +# "previous", for a one-step-back rollback point -- no-ops on the very first +# run, when there's no existing "latest" to promote). Deliberately just +# "latest"/"previous", not per-commit tags, to avoid accumulating history. +# See release.yml for tagged releases (git tag -> matching image tag). +# +# This project's CI/CD scope is intentionally just test/build/push -- +# deployment is being split into a separate project. +# +# Requires secrets.BUILD_API_TOKEN -- a personal access token +# (write:package scope) from the pushing account, stored manually as a repo +# secret. The auto-injected secrets.GITEA_TOKEN does NOT work for this: it +# never grants package-registry access regardless of the workflow's own +# `permissions:` block -- a known Gitea limitation, not a config mistake +# (https://github.com/go-gitea/gitea/issues/23642). + +on: + push: + branches: [main] + workflow_dispatch: + +jobs: + build-and-push: + runs-on: ubuntu-latest + # Relies on the Gitea runner's Docker daemon being reachable from job + # containers, which it is out of the box -- no extra runner config needed. + container: + image: docker:cli + steps: + # actions/checkout is a JS action; docker:cli is Alpine-based and has + # no node on PATH by default (same issue fixed in ci.yml). + - name: Install Node + run: apk add --no-cache nodejs + + - uses: actions/checkout@v4 + + - name: Log in to the container registry + env: + BUILD_API_TOKEN: ${{ secrets.BUILD_API_TOKEN }} + run: echo "$BUILD_API_TOKEN" | docker login code.aneur.in -u "${{ gitea.actor }}" --password-stdin + + - name: Promote the current "latest" to "previous" + run: | + IMAGE="code.aneur.in/${{ gitea.repository }}" + if docker pull "$IMAGE:latest"; then + docker tag "$IMAGE:latest" "$IMAGE:previous" + docker push "$IMAGE:previous" + fi + + - name: Build and push "latest" + run: | + IMAGE="code.aneur.in/${{ gitea.repository }}:latest" + docker build -t "$IMAGE" . + docker push "$IMAGE" diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..d4843e0 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,35 @@ +name: CI + +on: + pull_request: + branches: [main] + +jobs: + go-tests: + runs-on: ubuntu-latest + container: golang:1.25-alpine + steps: + # actions/checkout is a JS action -- golang:1.25-alpine has no node on + # PATH by default, so install it (musl-native, no glibc/Alpine mismatch) + # before any step that needs it. + - name: Install Node + run: apk add --no-cache nodejs + + - uses: actions/checkout@v4 + + - name: Verify dependencies + run: go mod verify + + - name: Vet + run: go vet ./... + + - name: Build + run: go build ./... + + - name: Test + run: go test ./... + + - name: Audit dependencies + run: | + go install golang.org/x/vuln/cmd/govulncheck@latest + "$(go env GOPATH)/bin/govulncheck" ./... diff --git a/.gitea/workflows/release.yml b/.gitea/workflows/release.yml new file mode 100644 index 0000000..f37bc10 --- /dev/null +++ b/.gitea/workflows/release.yml @@ -0,0 +1,48 @@ +name: Release + +# Builds and pushes an image tagged to match the git tag that triggered this +# run -- e.g. pushing tag "v1.2.3" produces code.aneur.in//:v1.2.3. +# Separate from build.yml's "latest"/"previous" tracking on push to main. +# This project's CI/CD scope is intentionally just test/build/push -- +# deployment (including how a given tag actually gets deployed) is being +# split into a separate project. +# +# Assumes tags are cut from commits already on main (and so already covered +# by ci.yml's checks) -- this workflow doesn't run the test suite itself. +# +# Requires secrets.BUILD_API_TOKEN -- a personal access token +# (write:package scope) from the pushing account, stored manually as a repo +# secret. The auto-injected secrets.GITEA_TOKEN does NOT work for this: it +# never grants package-registry access regardless of the workflow's own +# `permissions:` block -- a known Gitea limitation, not a config mistake +# (https://github.com/go-gitea/gitea/issues/23642). + +on: + push: + tags: ['*'] + +jobs: + build-and-push: + runs-on: ubuntu-latest + # Relies on the Gitea runner's Docker daemon being reachable from job + # containers, which it is out of the box -- no extra runner config needed. + container: + image: docker:cli + steps: + # actions/checkout is a JS action; docker:cli is Alpine-based and has + # no node on PATH by default (same issue fixed in ci.yml). + - name: Install Node + run: apk add --no-cache nodejs + + - uses: actions/checkout@v4 + + - name: Log in to the container registry + env: + BUILD_API_TOKEN: ${{ secrets.BUILD_API_TOKEN }} + run: echo "$BUILD_API_TOKEN" | docker login code.aneur.in -u "${{ gitea.actor }}" --password-stdin + + - name: Build and push the image + run: | + IMAGE="code.aneur.in/${{ gitea.repository }}:${{ gitea.ref_name }}" + docker build -t "$IMAGE" . + docker push "$IMAGE" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..036e7f7 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,19 @@ +# Build stage: compile a static binary so the runtime image can be minimal. +FROM golang:1.25-alpine AS build +WORKDIR /src +COPY go.mod go.sum ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 go build -trimpath -o /ultrashorty . + +# Runtime stage. +FROM alpine:3.20 +RUN adduser -D -u 10001 ultrashorty +USER ultrashorty +COPY --from=build /ultrashorty /usr/local/bin/ultrashorty +# The redirects CSV is supplied at deploy time (bind mount / config), and +# --host must be set to 0.0.0.0 to accept connections from outside the +# container -- ultrashorty defaults to localhost. Example: +# docker run -v ./redirects.csv:/redirects.csv --host 0.0.0.0 /redirects.csv +EXPOSE 4000 +ENTRYPOINT ["ultrashorty"]