From 269878948b8620bfe8f23cbb1cd9645b2d594257 Mon Sep 17 00:00:00 2001 From: Sean Gilligan Date: Mon, 19 Jan 2026 15:42:01 -0800 Subject: [PATCH] Add Nix Jekyll Build, GitHub Actions artifact, static Java site server --- .github/workflows/jekyll-build.yml | 40 +++++++++++++ README.md | 29 +++++++++ flake.lock | 61 +++++++++++++++++++ flake.nix | 94 ++++++++++++++++++++++++++++++ scripts/JekyllServer.java | 77 ++++++++++++++++++++++++ 5 files changed, 301 insertions(+) create mode 100644 .github/workflows/jekyll-build.yml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100755 scripts/JekyllServer.java diff --git a/.github/workflows/jekyll-build.yml b/.github/workflows/jekyll-build.yml new file mode 100644 index 000000000..a14075522 --- /dev/null +++ b/.github/workflows/jekyll-build.yml @@ -0,0 +1,40 @@ +name: Build Jekyll Site to Tarball + +on: + push: + branches: + - master + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Install Nix + uses: DeterminateSystems/nix-installer-action@main + + - name: Setup Nix cache + uses: DeterminateSystems/magic-nix-cache-action@main + + - name: Build site and create tarball + run: nix run .#tarball + + - name: Upload tarball as artifact + uses: actions/upload-artifact@v4 + with: + name: bitcoinj-site-${{ github.sha }} + path: bitcoinj-site.tar.gz + retention-days: 30 + + - name: Create release on tag + if: startsWith(github.ref, 'refs/tags/') + uses: softprops/action-gh-release@v1 + with: + files: bitcoinj-site.tar.gz + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/README.md b/README.md index 8c78f6ac4..6599606f4 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,35 @@ This is the source-code repository for the **bitcoinj** website, published at [b This site is written in [GitHub-flavored Markdown](https://docs.github.com/en/get-started/writing-on-github/getting-started-with-writing-and-formatting-on-github/about-writing-and-formatting-on-github), built using the [Jekyll static site generator](https://jekyllrb.com) and published via [GitHub Pages](https://pages.github.com). +## Building locally in a Nix Shell + +The Nix development shell will install Jekyll for building and serving the site and Java 25 (if you want a simple Java-based tool to serve/test the static site.) + +1. Run `nix develop` +2. Run `jekyll build` + +This will build the site into the `_site` directory. + +## Serving the site locally + +To serve the site with Jekyll and dynamic reloading: + +* `jekyll serve --livereload --incremental` + +To run a simple, local Java-based webserver to view the site use: + +* `./scripts/JekyllServer.java` + +## Building locally without Nix + +If you install Jekyll 3.10.0 and Ruby 3.3, you should be able to use the same commands as shown above, but this is untested/unsupported. You should also be able to serve the generated static site with the Java 25 script. + +## Building as a Nix Package + +* `nix build .#` + +This should build the package in `result`. + ## Contributing To report a documentation issue or make a suggestion for improvement use [GitHub Issues](). You can also submit a [pull-request](https://github.com/bitcoinj/bitcoinj.github.io/pulls) for the website. diff --git a/flake.lock b/flake.lock new file mode 100644 index 000000000..102eede61 --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1768857289, + "narHash": "sha256-WQOnDfQ2LTDq/5l4Qlm8TCY8fVL/5Fwg+D/YFDur8b4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ace813ce39b0536e03db81e1fb45d60734feb7ae", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "release-25.11", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 000000000..477c8a25b --- /dev/null +++ b/flake.nix @@ -0,0 +1,94 @@ +{ + description = "Development environment for bitcoinj.github.io Jekyll site"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/release-25.11"; + flake-utils.url = "github:numtide/flake-utils"; + }; + + outputs = { self, nixpkgs, flake-utils }: + flake-utils.lib.eachDefaultSystem (system: + let + pkgs = nixpkgs.legacyPackages.${system}; + + # Ruby environment with Jekyll and bundler + rubyEnv = pkgs.ruby_3_3.withPackages (ps: with ps; [ + jekyll + kramdown-parser-gfm + ]); + + in + { + devShells.default = pkgs.mkShell { + buildInputs = with pkgs; [ + rubyEnv + jdk25_headless + ]; + + shellHook = '' + echo "=== bitcoinj.github.io development environment ===" + echo "" + echo "Available commands:" + echo " jekyll build - Build the site to _site/" + echo " jekyll serve - Serve the site locally at http://localhost:4000" + echo " jekyll serve --livereload - Serve with auto-reload on changes" + echo " ./scripts/JekyllServer.java - Serve the static site with a Java script" + echo "" + ''; + }; + + # Package that builds the site + packages.default = pkgs.stdenv.mkDerivation { + pname = "bitcoinj-site"; + version = "0.0.1"; + + src = ./.; + + buildInputs = [ rubyEnv ]; + + buildPhase = '' + export HOME=$TMPDIR + jekyll build + ''; + + installPhase = '' + mkdir -p $out + cp -r _site/* $out/ + ''; + }; + + # App for easy site building + apps.build = { + type = "app"; + program = "${pkgs.writeShellScript "build-site" '' + set -e + export HOME=$TMPDIR + ${rubyEnv}/bin/jekyll build + echo "Site built to _site/" + ''}"; + }; + + apps.serve = { + type = "app"; + program = "${pkgs.writeShellScript "serve-site" '' + set -e + export HOME=$TMPDIR + ${rubyEnv}/bin/jekyll serve --livereload + ''}"; + }; + + apps.tarball = { + type = "app"; + program = "${pkgs.writeShellScript "create-tarball" '' + set -e + export HOME=$TMPDIR + echo "Building site..." + ${rubyEnv}/bin/jekyll build + echo "Creating tarball..." + ${pkgs.gnutar}/bin/tar -czf bitcoinj-site.tar.gz -C _site . + echo "Tarball created: bitcoinj-site.tar.gz" + ''}"; + }; + } + ); +} diff --git a/scripts/JekyllServer.java b/scripts/JekyllServer.java new file mode 100755 index 000000000..e2aa0bf82 --- /dev/null +++ b/scripts/JekyllServer.java @@ -0,0 +1,77 @@ +///usr/bin/env java "$0" "$@" ; exit $? +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpHandlers; +import com.sun.net.httpserver.SimpleFileServer; +import java.net.InetSocketAddress; +import java.nio.file.Path; + +static final int port = 4000; + +public static void main(String[] args) { + Path root = Path.of("./_site").toAbsolutePath(); + + // Create the default file handler + var fileHandler = SimpleFileServer.createFileHandler(root); + + // This handler checks if we need to append .html internally + HttpHandler rewritingHandler = exchange -> { + String path = exchange.getRequestURI().getPath(); + + // If path is not root, doesn't have a dot, and doesn't end in a slash + if (!path.equals("/") && !path.contains(".") && !path.endsWith("/")) { + String newPath = path + ".html"; + + // We wrap the exchange to override the Request URI + // The file handler will now look for "name.html" on disk + fileHandler.handle(new WrappedExchange(exchange, newPath)); + } else { + fileHandler.handle(exchange); + } + }; + + // Start the server + var server = SimpleFileServer.createFileServer( + new InetSocketAddress(port), + root, + SimpleFileServer.OutputLevel.INFO + ); + + // Replace the default context with our custom handler + server.removeContext("/"); + server.createContext("/", rewritingHandler); + + System.out.printf("Serving Jekyll site at http://localhost:%s\n", port); + server.start(); + IO.println("Use Control-C to cancel"); +} + +// Helper class to "trick" the file handler into looking for a different path +static class WrappedExchange extends HttpExchange { + private final HttpExchange delegate; + private final URI interceptedUri; + + WrappedExchange(HttpExchange delegate, String newPath) { + this.delegate = delegate; + this.interceptedUri = delegate.getRequestURI().resolve(newPath); + } + + @Override public URI getRequestURI() { return interceptedUri; } + // Delegate all other methods + @Override public com.sun.net.httpserver.Headers getRequestHeaders() { return delegate.getRequestHeaders(); } + @Override public com.sun.net.httpserver.Headers getResponseHeaders() { return delegate.getResponseHeaders(); } + @Override public String getRequestMethod() { return delegate.getRequestMethod(); } + @Override public void sendResponseHeaders(int rCode, long responseLength) throws java.io.IOException { delegate.sendResponseHeaders(rCode, responseLength); } + @Override public java.io.InputStream getRequestBody() { return delegate.getRequestBody(); } + @Override public java.io.OutputStream getResponseBody() { return delegate.getResponseBody(); } + @Override public void close() { delegate.close(); } + @Override public InetSocketAddress getRemoteAddress() { return delegate.getRemoteAddress(); } + @Override public int getResponseCode() { return delegate.getResponseCode(); } + @Override public InetSocketAddress getLocalAddress() { return delegate.getLocalAddress(); } + @Override public String getProtocol() { return delegate.getProtocol(); } + @Override public Object getAttribute(String name) { return delegate.getAttribute(name); } + @Override public void setAttribute(String name, Object value) { delegate.setAttribute(name, value); } + @Override public void setStreams(java.io.InputStream i, java.io.OutputStream o) { delegate.setStreams(i, o); } + @Override public com.sun.net.httpserver.HttpContext getHttpContext() { return delegate.getHttpContext(); } + @Override public com.sun.net.httpserver.HttpPrincipal getPrincipal() { return delegate.getPrincipal(); } +} \ No newline at end of file