Skip to content

Commit 48a2676

Browse files
committed
feat: add automated server version updates and container builds
1 parent 003091c commit 48a2676

File tree

10 files changed

+346
-0
lines changed

10 files changed

+346
-0
lines changed
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
name: Update Servers
2+
3+
on:
4+
schedule:
5+
- cron: '0 0 * * 0' # Weekly on Sunday
6+
workflow_dispatch:
7+
8+
permissions:
9+
contents: write
10+
packages: write
11+
12+
jobs:
13+
update:
14+
runs-on: ubuntu-latest
15+
outputs:
16+
has_updates: ${{ steps.update.outputs.has_updates }}
17+
updated_servers: ${{ steps.update.outputs.updated_servers }}
18+
steps:
19+
- uses: actions/checkout@v4
20+
21+
- name: Install uv
22+
uses: astral-sh/setup-uv@v7
23+
with:
24+
python-version: '3.12'
25+
26+
- name: Run update script
27+
id: update
28+
run: uv run scripts/update_containers.py
29+
env:
30+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
31+
32+
- name: Commit and push changes
33+
if: steps.update.outputs.has_updates == 'true'
34+
run: |
35+
git config --global user.name "github-actions[bot]"
36+
git config --global user.email "github-actions[bot]@users.noreply.github.com"
37+
git add container/
38+
git commit -m "chore: update server versions"
39+
git push
40+
41+
build:
42+
needs: update
43+
if: needs.update.outputs.has_updates == 'true'
44+
runs-on: ubuntu-latest
45+
strategy:
46+
matrix:
47+
server: ${{ fromJson(needs.update.outputs.updated_servers) }}
48+
steps:
49+
- uses: actions/checkout@v4
50+
51+
- name: Log in to GHCR
52+
uses: docker/login-action@v3
53+
with:
54+
registry: ghcr.io
55+
username: ${{ github.actor }}
56+
password: ${{ secrets.GITHUB_TOKEN }}
57+
58+
- name: Get version
59+
id: get_version
60+
run: |
61+
VERSION=$(grep "ARG VERSION=" container/${{ matrix.server }}/ContainerFile | cut -d'=' -f2)
62+
echo "version=$VERSION" >> $GITHUB_OUTPUT
63+
64+
- name: Build and push
65+
uses: docker/build-push-action@v6
66+
with:
67+
context: container/${{ matrix.server }}
68+
file: container/${{ matrix.server }}/ContainerFile
69+
push: true
70+
tags: |
71+
ghcr.io/${{ github.repository }}/${{ matrix.server }}:${{ steps.get_version.outputs.version }}
72+
ghcr.io/${{ github.repository }}/${{ matrix.server }}:latest

container/deno/ContainerFile

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
# Deno has built-in LSP
2+
ARG VERSION=2.6.3
3+
FROM denoland/deno:alpine-${VERSION}
4+
ARG VERSION
5+
LABEL org.opencontainers.image.version="${VERSION}"
6+
7+
WORKDIR /workspace
8+
ENTRYPOINT ["deno", "lsp"]

container/pyrefly/ContainerFile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
# Multi-stage build for Pyrefly
2+
ARG VERSION=0.46.0
3+
FROM python:3.12-slim AS builder
4+
ARG VERSION
5+
RUN pip install --no-cache-dir pyrefly==${VERSION}
6+
7+
FROM python:3.12-slim
8+
LABEL org.opencontainers.image.version="${VERSION}"
9+
COPY --from=builder /usr/local/lib/python3.12/site-packages /usr/local/lib/python3.12/site-packages
10+
COPY --from=builder /usr/local/bin/pyrefly /usr/local/bin/pyrefly
11+
12+
WORKDIR /workspace
13+
ENTRYPOINT ["pyrefly", "lsp"]

container/pyright/ContainerFile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Multi-stage build for Pyright
2+
ARG VERSION=1.1.407
3+
FROM node:22-slim AS builder
4+
ARG VERSION
5+
RUN npm install -g pyright@${VERSION}
6+
7+
FROM node:22-slim
8+
LABEL org.opencontainers.image.version="${VERSION}"
9+
COPY --from=builder /usr/local/lib/node_modules /usr/local/lib/node_modules
10+
COPY --from=builder /usr/local/bin/pyright /usr/local/bin/pyright
11+
COPY --from=builder /usr/local/bin/pyright-langserver /usr/local/bin/pyright-langserver
12+
13+
# Pyright needs node to run
14+
WORKDIR /workspace
15+
ENTRYPOINT ["pyright-langserver", "--stdio"]
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Multi-stage build for Rust-analyzer
2+
ARG VERSION=2025-12-15
3+
FROM debian:bookworm-slim AS builder
4+
ARG VERSION
5+
RUN apt-get update && apt-get install -y curl && \
6+
curl -L https://github.com/rust-lang/rust-analyzer/releases/download/${VERSION}/rust-analyzer-x86_64-unknown-linux-gnu.gz | gunzip -c - > /usr/local/bin/rust-analyzer && \
7+
chmod +x /usr/local/bin/rust-analyzer
8+
9+
FROM debian:bookworm-slim
10+
LABEL org.opencontainers.image.version="${VERSION}"
11+
RUN apt-get update && apt-get install -y libgcc-s1 && rm -rf /var/lib/apt/lists/*
12+
COPY --from=builder /usr/local/bin/rust-analyzer /usr/local/bin/rust-analyzer
13+
14+
WORKDIR /workspace
15+
ENTRYPOINT ["rust-analyzer"]

container/typescript/ContainerFile

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
# Multi-stage build for TypeScript Language Server
2+
ARG VERSION=5.1.3
3+
FROM node:22-slim AS builder
4+
ARG VERSION
5+
RUN npm install -g typescript-language-server@${VERSION} typescript
6+
7+
FROM node:22-slim
8+
LABEL org.opencontainers.image.version="${VERSION}"
9+
COPY --from=builder /usr/local/lib/node_modules /usr/local/lib/node_modules
10+
COPY --from=builder /usr/local/bin/typescript-language-server /usr/local/bin/typescript-language-server
11+
COPY --from=builder /usr/local/bin/tsserver /usr/local/bin/tsserver
12+
COPY --from=builder /usr/local/bin/tsc /usr/local/bin/tsc
13+
14+
WORKDIR /workspace
15+
ENTRYPOINT ["typescript-language-server", "--stdio"]

registry/wiki.schema.json

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
{
2+
"$schema": "http://json-schema.org/draft-07/schema#",
3+
"title": "LSP Servers Registry",
4+
"type": "object",
5+
"additionalProperties": {
6+
"type": "object",
7+
"required": ["type"],
8+
"properties": {
9+
"type": {
10+
"enum": ["npm", "pypi", "github", "custom"]
11+
},
12+
"package": {
13+
"type": "string"
14+
},
15+
"repo": {
16+
"type": "string"
17+
},
18+
"command": {
19+
"type": "string"
20+
},
21+
"strip_v": {
22+
"type": "boolean"
23+
}
24+
},
25+
"if": {
26+
"properties": { "type": { "const": "npm" } }
27+
},
28+
"then": {
29+
"required": ["package"]
30+
},
31+
"else": {
32+
"if": {
33+
"properties": { "type": { "const": "pypi" } }
34+
},
35+
"then": {
36+
"required": ["package"]
37+
},
38+
"else": {
39+
"if": {
40+
"properties": { "type": { "const": "github" } }
41+
},
42+
"then": {
43+
"required": ["repo"]
44+
},
45+
"else": {
46+
"if": {
47+
"properties": { "type": { "const": "custom" } }
48+
},
49+
"then": {
50+
"required": ["command"]
51+
}
52+
}
53+
}
54+
}
55+
}
56+
}

registry/wiki.toml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
[pyright]
2+
type = "npm"
3+
package = "pyright"
4+
5+
[typescript]
6+
type = "npm"
7+
package = "typescript-language-server"
8+
9+
[rust-analyzer]
10+
type = "github"
11+
repo = "rust-lang/rust-analyzer"
12+
13+
[pyrefly]
14+
type = "pypi"
15+
package = "pyrefly"
16+
17+
[deno]
18+
type = "github"
19+
repo = "denoland/deno"
20+
strip_v = true
21+
22+
# [custom-server]
23+
# type = "custom"
24+
# command = "curl -s https://api.example.com/version | jq -r .version"

scripts/server_versions.py

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import subprocess
5+
import sys
6+
import tomllib
7+
import urllib.request
8+
9+
10+
def get_latest_npm(package):
11+
with urllib.request.urlopen(
12+
f"https://registry.npmjs.org/{package}/latest"
13+
) as response:
14+
return json.load(response)["version"]
15+
16+
17+
def get_latest_pypi(package):
18+
with urllib.request.urlopen(f"https://pypi.org/pypi/{package}/json") as response:
19+
return json.load(response)["info"]["version"]
20+
21+
22+
def get_latest_github_release(repo):
23+
request = urllib.request.Request(
24+
f"https://api.github.com/repos/{repo}/releases/latest"
25+
)
26+
request.add_header("User-Agent", "lsp-client-updater")
27+
import os
28+
29+
token = os.environ.get("GITHUB_TOKEN")
30+
if token:
31+
request.add_header("Authorization", f"Bearer {token}")
32+
with urllib.request.urlopen(request) as response:
33+
return json.load(response)["tag_name"]
34+
35+
36+
def get_latest_custom(command: str) -> str:
37+
result = subprocess.run(
38+
command, shell=True, capture_output=True, text=True, check=True
39+
)
40+
return result.stdout.strip()
41+
42+
43+
def main():
44+
with open("registry/wiki.toml", "rb") as f:
45+
wiki = tomllib.load(f)
46+
47+
versions = {}
48+
for server, config in wiki.items():
49+
if not isinstance(config, dict):
50+
continue
51+
try:
52+
if config["type"] == "npm":
53+
versions[server] = get_latest_npm(config["package"])
54+
elif config["type"] == "pypi":
55+
versions[server] = get_latest_pypi(config["package"])
56+
elif config["type"] == "github":
57+
v = get_latest_github_release(config["repo"])
58+
if config.get("strip_v"):
59+
v = v.lstrip("v")
60+
versions[server] = v
61+
elif config["type"] == "custom":
62+
versions[server] = get_latest_custom(config["command"])
63+
except Exception as e:
64+
print(f"Error fetching version for {server}: {e}", file=sys.stderr)
65+
continue
66+
67+
print(json.dumps(versions, indent=2))
68+
69+
70+
if __name__ == "__main__":
71+
main()

scripts/update_containers.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
from __future__ import annotations
2+
3+
import json
4+
import os
5+
import re
6+
import subprocess
7+
import sys
8+
from pathlib import Path
9+
10+
11+
def update_container_file(server_name: str, version: str) -> bool:
12+
container_file = Path(f"container/{server_name}/ContainerFile")
13+
if not container_file.exists():
14+
print(f"ContainerFile for {server_name} not found at {container_file}")
15+
return False
16+
17+
content = container_file.read_text()
18+
new_content = re.sub(r"ARG VERSION=.*", f"ARG VERSION={version}", content, count=1)
19+
20+
if content != new_content:
21+
container_file.write_text(new_content)
22+
print(f"Updated {server_name} to {version}")
23+
return True
24+
25+
return False
26+
27+
28+
def main():
29+
# Capture the output of server_versions.py
30+
result = subprocess.run(
31+
[sys.executable, "scripts/server_versions.py"],
32+
capture_output=True,
33+
text=True,
34+
check=True,
35+
)
36+
versions = json.loads(result.stdout)
37+
38+
updated_servers = []
39+
for server, version in versions.items():
40+
if update_container_file(server, version):
41+
updated_servers.append(server)
42+
43+
if updated_servers:
44+
print(f"Updated servers: {', '.join(updated_servers)}")
45+
if "GITHUB_OUTPUT" in os.environ:
46+
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
47+
f.write(f"updated_servers={json.dumps(updated_servers)}\n")
48+
f.write("has_updates=true\n")
49+
else:
50+
print("No updates found.")
51+
if "GITHUB_OUTPUT" in os.environ:
52+
with open(os.environ["GITHUB_OUTPUT"], "a") as f:
53+
f.write("has_updates=false\n")
54+
55+
56+
if __name__ == "__main__":
57+
main()

0 commit comments

Comments
 (0)