โš’Anvil
Sign in

clo / anvil public

๐Ÿ“ciImporting into a read-only forge: only what the public sees there
๐Ÿ“sourceImporting into a read-only forge: only what the public sees there
๐Ÿ“staticRead-only: a forge that only shows, ANVIL_READ_ONLY
๐Ÿ“„.gitignoreIngot moves out to a repository of its own; Anvil takes it as a dependency
๐Ÿ“„.gitlab-ci.ymlThe runner's tag chosen by the GitLab the pipeline runs on
๐Ÿ“„ARCHITECTURE.mdIngot moves out to a repository of its own; Anvil takes it as a dependency
๐Ÿ“„LICENSEAnvil and Ingot: a forge in Common Lisp and Gendl, first working cut
๐Ÿ“„README.mdImporting into a read-only forge: only what the public sees there
๐Ÿ“„anvil.asdFetching and importing from GitLab: Ingot's client side, a group and its projects brought over
๐Ÿ“„around-compile.sexpAnvil and Ingot: a forge in Common Lisp and Gendl, first working cut
๐Ÿ“„author.sexpAnvil and Ingot: a forge in Common Lisp and Gendl, first working cut
๐Ÿ“„ci.sexpIngot moves out to a repository of its own; Anvil takes it as a dependency
๐Ÿ“„depends-on.sexpAnvil and Ingot: a forge in Common Lisp and Gendl, first working cut
๐Ÿ“„description.sexpAnvil and Ingot: a forge in Common Lisp and Gendl, first working cut
๐Ÿ“„dev.lispRead-only: a forge that only shows, ANVIL_READ_ONLY
๐Ÿ“„ignore-list.sexpAnvil and Ingot: a forge in Common Lisp and Gendl, first working cut
๐Ÿ“„license.sexpAnvil and Ingot: a forge in Common Lisp and Gendl, first working cut

README.md

Anvil

A software forge in Common Lisp and Gendl: repositories served over Git's smart HTTP protocol, a web face for browsing them, issues, merge requests, continuous integration that runs a project's .gitlab-ci.yml or a native s-expression pipeline (ci.sexp), a REST API in GitLab's shapes, an OAuth2 provider, and a push-button deployment of a project into a house that hosts applications. Built first for common-lisp.net, whose services lean on exactly these features of GitLab, so that what talks to GitLab there keeps working when pointed at Anvil.

Git itself is Ingot, a Git implementation in plain Common Lisp and a repository of its own, which Anvil takes as a dependency: loose and packed objects (deltas included), refs, trees, commits, diffs, three-way merges, the smart protocol on both sides, archives. No subprocess and no git binary anywhere. Anvil finds it in INGOT_DIR, else in a checkout beside this one (../ingot/), else in ingot/ inside it, where the pipeline unpacks it.

ARCHITECTURE.md is the design.

Running it

In a Gendl image (the genworks/gendl:devo-ccl or devo-sbcl images, or any image with Gendl loaded) that can see this checkout:

(load "/path/to/anvil/dev.lisp")

Anvil answers at http://localhost:9095/, with its state under ANVIL_DATA_DIR (else /tmp/anvil-dev/). Register the first account at /users/sign_up: the first account is an administrator. Make a project, then:

$ git clone http://localhost:9095/you/project.git
$ git push http://you:<token>@localhost:9095/you/project.git master

A personal access token (/-/profile/tokens) stands in for the password with git and with the API (PRIVATE-TOKEN: <token>).

Configuring a site

Everything a site sets is a special variable of the anvil package, set before initialize! or start!:

| Variable | What | | --- | --- | | *site-name*, *site-url* | The forge's name and public address (clone addresses and API links are made from it). | | *hosts* | Host names to answer to, or nil for all. | | *data-directory* | Where everything lives: db/ (records, one s-expression file each), repositories/, ci/. | | *registration-open?* | Whether anyone may register. | | *docker-socket* | The Docker daemon's Unix socket (mount it into the container), or tcp://host:port; nil switches the executor off. | | *runner-tags*, *runner-parallelism* | What jobs this image's runner takes, and how many at once. | | *deploy-houses* | The houses a project may be deployed to: (:key :common-lisp.app :name "..." :lab-url "https://lab.common-lisp.app/prompt-lab" :secret-file #p"..." :domain "common-lisp.app"). |

| *read-only?* | A forge that only shows; see below. Set from ANVIL_READ_ONLY. |

initialize! publishes the forge on an existing AllegroServe server (for a Gendl image that serves other things too, scoped by *hosts*); start! starts a server of Anvil's own on a port.

Read-only

With the environment variable ANVIL_READ_ONLY set to anything non-empty (ANVIL_READ_ONLY=1), the forge only shows. Browsing works as before, clones and fetches included. Every change over HTTP is refused with a 403, whichever page or program asks: signing up or in, tokens and keys, new projects, groups, issues and merge requests, comments, edits, settings, the API's writing verbs, OAuth2 (no token is issued) and pushes (git prints the refusal). Refusals come back as a page, as JSON for the API and OAuth, and as plain text for git. The pages keep every control that would change something but grey it out, with a title saying the forge is read-only for now.

dev.lisp, ci/runner-smoke.lisp and the lab rig's ci/lab-e2e/anvil-init.lisp read the variable into *read-only?*, and initialize! sets it as well whenever the variable is there, so a start-up file of your own can't leave such a forge writable. Functions called at the REPL still write. That is how a read-only forge gets loaded and kept current: make-user!, import-from-gitlab! (run it again to refresh), fetch-repository!.

Continuous integration

A project with a ci.sexp or a .gitlab-ci.yml at its root runs a pipeline on every push. Both compile to the same thing; ci.sexp is read first. Jobs run in containers through the Docker Engine API, their working tree handed in as a tar made from the commit (no git in the job image), their output followed into a trace, their artifacts fetched out. A job tagged shell may run on the image's own host when *shell-executor?* is on.

(:pipeline
 :stages (:test :deploy)
 :default (:image "genworks/gendl:devo-ccl")
 :variables (("GIT_DEPTH" "1"))
 :jobs
 ((:load-and-smoke :stage :test :lisp (:load ("ci/test.lisp")))
  (:stylesheet :stage :test :image "node:22-slim"
               :script ("cd tailwind" "npm ci" "npm run check"))
  (:deploy :stage :deploy
           :rules ((:if (:and (:default-branch) (:variable "DEPLOY" "on"))))
           :deploy (:house :common-lisp.app))))

The API

/api/v4/, in GitLab's names and shapes: projects, groups, members, repository trees, blobs, files, branches, tags, commits (reading and making), archives, issues and notes, merge requests (reading, making, merging), pipelines, jobs, traces, artifacts (by job, or the latest of a ref), CI variables, webhooks. See ARCHITECTURE.md for the list.

Deployment

A Maintainer of a public project that meets the hosting house's profile (an image.sexp and a monetize.sexp at its root; checked in process when Monocle is loaded) presses Deploy, and the forge hands the house's lab a hosting request with the shared secret; the Deployments page follows the request to its address. A :deploy job does the same at the end of a green pipeline, and a project may ask to be deployed on every green pipeline of its default branch.

Importing from GitLab

A group with its projects, or single projects, can be brought over from a GitLab: the repositories through Ingot's fetch (every branch and tag, HEAD too), the issues and merge requests with their notes through GitLab's API, their numbers kept so that a link which worked there works here.

(anvil:import-from-gitlab! "https://gitlab.common-lisp.net"
                           :token "<a personal access token with read_api>"  ; public things need none
                           :groups '("clo" ("alexandria" "alexandria"))      ; a group whole, or some of its projects
                           :projects '("someone/a-project"))                   ; single projects, into the namespace of that path

People are not imported. What has no author here is authored by the importing user (the first administrator, unless :importer names another), and its text begins with a line saying who wrote it there and when; a user of the same username here is matched instead. Importing again brings a repository up to date and adds only the issues and merge requests not here yet. :into "a-group" puts single projects under one group here; :auth '("oauth2" . "<token>") fetches repositories that are not public.

On a read-only forge, which serves the public, the importer brings only what the public sees on the GitLab, whatever the token can see: a project that is private or internal there is left out (its report says :skipped), and so are a public project's issues or merge requests when they are open to members only. :public-only? nil brings everything. A project's issues or merge requests that fail to come are reported (:issues-error, :merge-requests-error) and the import goes on with the rest. The fetch calls itself git/anvil-import, because common-lisp.net's bot challenge passes git clients by their user agent and challenges anything else.

Checks

Ingot's own, in its repository: test/test.lisp into an image that has the system loaded (INGOT_REPO=/path/to/some/.git makes it read a real repository too), or its ci/test.lisp cold. Anvil's cold load and smoke test is ci/test.lisp, run by the pipeline inside a Gendl image that has never seen the checkout.

License

GNU Affero General Public License, version 3 or later; see LICENSE.