Compare commits

..

2 commits

Author SHA1 Message Date
c450271582
cli: add support for GHA based update action 2024-09-10 11:38:45 -05:00
3e0609d1f8 flake.lock: Update
Flake lock file updates:

• Updated input 'hyprland':
    'git+https://github.com/hyprwm/Hyprland/?ref=refs/heads/main&rev=04421063af2941c6e27e6dca2bdc2c387778a3a5' (2024-09-09)
  → 'git+https://github.com/hyprwm/Hyprland/?ref=refs/heads/main&rev=155d44016d0cb11332c454db73d59030cdbd7b13' (2024-09-10)
• Updated input 'hyprland/xdph':
    'github:hyprwm/xdg-desktop-portal-hyprland/2425e8f541525fa7409d9f26a8ffaf92a3767251' (2024-09-01)
  → 'github:hyprwm/xdg-desktop-portal-hyprland/11e15b437e7efc39e452f36e15a183225d6bfa39' (2024-09-01)
• Updated input 'lix':
    'c14486ae8d.tar.gz?narHash=sha256-8tzJO3PllVPc0RYE0OfXVWlgTiJxKH1nzXsQLGyFRJ4%3D&rev=c14486ae8d3bbc862c625d948a6b2f4dc0927d5b' (2024-09-09)
  → 'cc183fdbc1.tar.gz?narHash=sha256-tiQ9OxiuTb/02xEU2ceo9MIxWBS5Rm/IAhv6QshH8K0%3D&rev=cc183fdbc14ce105a5661d646983f791978b9d5c' (2024-09-10)
• Updated input 'nixpkgs-wayland':
    'github:nix-community/nixpkgs-wayland/dc951da2b8fdaabc09a1bc72dd5744438976be47' (2024-09-09)
  → 'github:nix-community/nixpkgs-wayland/90046312d6c074e7b941b7ea9c4e54f4d416e5da' (2024-09-10)
• Updated input 'roc':
    'github:roc-lang/roc/9a4d55672551fb4ffb54983272bb02d119c19f85' (2024-09-07)
  → 'github:roc-lang/roc/2936a37a1c54cb4cb10003c3b7e43a4772bbccf9' (2024-09-10)
2024-09-10 16:14:20 +00:00
8 changed files with 219 additions and 51 deletions

View file

@ -323,11 +323,11 @@
"xdph": "xdph" "xdph": "xdph"
}, },
"locked": { "locked": {
"lastModified": 1725873008, "lastModified": 1725976150,
"narHash": "sha256-hVPjlB0EPbf98tm4LcwOmS80tO/qfAoXqXKWZjDUG50=", "narHash": "sha256-Dv4XEWRcVFZhBDbj11/zuuXyf7TGHFVU1IGH9W/yPX8=",
"ref": "refs/heads/main", "ref": "refs/heads/main",
"rev": "04421063af2941c6e27e6dca2bdc2c387778a3a5", "rev": "155d44016d0cb11332c454db73d59030cdbd7b13",
"revCount": 5203, "revCount": 5209,
"submodules": true, "submodules": true,
"type": "git", "type": "git",
"url": "https://github.com/hyprwm/Hyprland/" "url": "https://github.com/hyprwm/Hyprland/"
@ -504,11 +504,11 @@
"lix": { "lix": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1725846500, "lastModified": 1725927421,
"narHash": "sha256-8tzJO3PllVPc0RYE0OfXVWlgTiJxKH1nzXsQLGyFRJ4=", "narHash": "sha256-tiQ9OxiuTb/02xEU2ceo9MIxWBS5Rm/IAhv6QshH8K0=",
"rev": "c14486ae8d3bbc862c625d948a6b2f4dc0927d5b", "rev": "cc183fdbc14ce105a5661d646983f791978b9d5c",
"type": "tarball", "type": "tarball",
"url": "https://git.lix.systems/api/v1/repos/lix-project/lix/archive/c14486ae8d3bbc862c625d948a6b2f4dc0927d5b.tar.gz?rev=c14486ae8d3bbc862c625d948a6b2f4dc0927d5b" "url": "https://git.lix.systems/api/v1/repos/lix-project/lix/archive/cc183fdbc14ce105a5661d646983f791978b9d5c.tar.gz?rev=cc183fdbc14ce105a5661d646983f791978b9d5c"
}, },
"original": { "original": {
"type": "tarball", "type": "tarball",
@ -744,11 +744,11 @@
"nixpkgs": "nixpkgs_6" "nixpkgs": "nixpkgs_6"
}, },
"locked": { "locked": {
"lastModified": 1725885379, "lastModified": 1725981791,
"narHash": "sha256-gw+CYMQRqzErAIp4WOTTeX6YXOhgk9YWyTM1Sa2cACA=", "narHash": "sha256-+4dwaoIrnubM29MK8BW4S2mdKwdlCF1svtO0hQ443X0=",
"owner": "nix-community", "owner": "nix-community",
"repo": "nixpkgs-wayland", "repo": "nixpkgs-wayland",
"rev": "dc951da2b8fdaabc09a1bc72dd5744438976be47", "rev": "90046312d6c074e7b941b7ea9c4e54f4d416e5da",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -900,11 +900,11 @@
"rust-overlay": "rust-overlay" "rust-overlay": "rust-overlay"
}, },
"locked": { "locked": {
"lastModified": 1725679186, "lastModified": 1725957803,
"narHash": "sha256-Eq+AI70CYMpIAhCjJ0VOoafd3tVhRYgXi8CzEqDn0KI=", "narHash": "sha256-qBG8DEmc9aOLr/WBtsuOB5QKEDxK2bDy4dq3X686xdo=",
"owner": "roc-lang", "owner": "roc-lang",
"repo": "roc", "repo": "roc",
"rev": "9a4d55672551fb4ffb54983272bb02d119c19f85", "rev": "2936a37a1c54cb4cb10003c3b7e43a4772bbccf9",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -1162,11 +1162,11 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1725203932, "lastModified": 1725228143,
"narHash": "sha256-VLULC/OnI+6R9KEP2OIGk+uLJJsfRlaLouZ5gyFd2+Y=", "narHash": "sha256-kbSiPA5oXiz1+1eVoRslMi5wylHD6SDT8dS9eZAxXAM=",
"owner": "hyprwm", "owner": "hyprwm",
"repo": "xdg-desktop-portal-hyprland", "repo": "xdg-desktop-portal-hyprland",
"rev": "2425e8f541525fa7409d9f26a8ffaf92a3767251", "rev": "11e15b437e7efc39e452f36e15a183225d6bfa39",
"type": "github" "type": "github"
}, },
"original": { "original": {

View file

@ -1,5 +1,5 @@
## nix begat oizys ## nix begat oizys
import std/[os, tables, sequtils, strformat,] import std/[os, tables, sequtils, strformat,strutils]
import cligen, bbansi import cligen, bbansi
import oizys/[context, github, nix, overlay, logging] import oizys/[context, github, nix, overlay, logging]
@ -13,6 +13,17 @@ addHandler(
) )
) )
proc confirm(q: string): bool =
stderr.write $(q & bb"[yellow] (Y/n) ")
while true:
let ans = readLine(stdin)
case ans.strip().toLowerAscii():
of "y","yes": return true
of "n","no": return false
else:
stderr.write($bb("[red]Please answer Yes/no\nexpected one of [b]Y,yes,N,no "))
stderr.write "\n"
overlay: overlay:
proc pre( proc pre(
flake: string = "", flake: string = "",
@ -32,9 +43,20 @@ overlay:
## output ## output
echo nixosConfigAttrs().join(" ") echo nixosConfigAttrs().join(" ")
proc update(yes: bool = false) = proc update(
## *TBI* update and run nixos-rebuild yes: bool = false,
fatal "not implemented" preview: bool = false
) =
## update and run nixos-rebuild
let hosts = getHosts()
if hosts.len > 1: fatalQuit "operation only supports one host"
let run = getLastUpdateRun()
echo fmt"run created at: {run.created_at}"
echo "nvd diff:\n", getUpdateSummary(run.id, hosts[0])
if preview: quit 0
if yes or confirm("Proceed with system update?"):
updateRepo()
nixosRebuild("switch")
proc build(minimal: bool = false) = proc build(minimal: bool = false) =
## nix build ## nix build

View file

@ -1,11 +1,11 @@
import std/[logging, os, strformat, strutils] import std/[logging, os, strformat, strutils]
from std/nativesockets import getHostname from std/nativesockets import getHostname
import bbansi
import ./logging import ./logging
type type
OizysContext* = object OizysContext* = object
flake, host: string flake: string
hosts: seq[string] hosts: seq[string]
debug: bool debug: bool
ci: bool ci: bool
@ -39,13 +39,12 @@ proc updateContext*(
) = ) =
oc.debug = debug oc.debug = debug
oc.resetCache = resetCache oc.resetCache = resetCache
if host.len > 0:
oc.hosts = host
if flake != "": if flake != "":
oc.flake = oc.flake =
if flake.startsWith("github") or flake.startsWith("git+"): flake if flake.startsWith("github") or flake.startsWith("git+"): flake
else: checkPath(flake.normalizedPath().absolutePath()) else: checkPath(flake.normalizedPath().absolutePath())
debug oc
debug bb(fmt"""[b]flake[/]: {oc.flake}, [b]hosts[/]: {oc.hosts.join(" ")}""")
proc getHosts*(): seq[string] = return oc.hosts proc getHosts*(): seq[string] = return oc.hosts
proc getFlake*(): string = return oc.flake proc getFlake*(): string = return oc.flake

View file

@ -23,7 +23,7 @@ type
proc runCmdCapt*( proc runCmdCapt*(
cmd: string, cmd: string,
capture: set[CaptureGrp], capture: set[CaptureGrp] = {CaptStdout},
): tuple[stdout, stderr: string, exitCode: int] = ): tuple[stdout, stderr: string, exitCode: int] =
debug fmt"running cmd: {cmd}" debug fmt"running cmd: {cmd}"
let args = cmd.splitWhitespace() let args = cmd.splitWhitespace()

View file

@ -1,21 +1,43 @@
import std/[httpclient,logging, os, strformat, strutils, json] import std/[httpclient,logging, os, strformat, strutils, json, tables, tempfiles]
import ./logging import jsony, bbansi, zippy/ziparchives
import ./[logging, exec, context]
# localPassC is used by zippy but the additional
# module mangling on nixos somehow breaks localPassC
when defined(amd64) and (defined(gcc) or defined(clang)):
{.passC: "-msse4.1 -mpclmul".}
template withTmpDir(body: untyped): untyped =
let tmpDir {.inject.} = createTempDir("oizys","")
body
removeDir(tmpDir)
var ghToken = getEnv("GITHUB_TOKEN") var ghToken = getEnv("GITHUB_TOKEN")
proc checkToken() {.inline.} = proc checkToken() {.inline.} =
if ghToken == "": fatalQuit "GITHUB_TOKEN not set" if ghToken == "": fatalQuit "GITHUB_TOKEN not set"
#[curl -L \ proc ghClient(
-X POST \ maxRedirects = 5
-H "Accept: application/vnd.github+json" \ ): HttpClient =
-H "Authorization: Bearer <YOUR-TOKEN>" \ checkToken()
-H "X-GitHub-Api-Version: 2022-11-28" \ result = newHttpClient(maxRedirects = maxRedirects)
https://api.github.com/repos/OWNER/REPO/actions/workflows/WORKFLOW_ID/dispatches \ result.headers = newHttpHeaders({
-d '{"ref":"topic-branch","inputs":{"name":"Mona the Octocat","home":"San Francisco, CA"}}' "Accept" : "application/vnd.github+json",
]# "Authorization" : fmt"Bearer {ghToken}",
"X-GitHub-Api-Version": "2022-11-28",
})
proc getGhApi(url: string): Response =
let client = ghClient()
try:
result = client.get(url)
except:
error fmt"github api request failed: {url}"
error fmt"response: {result.body}"
quit QuitFailure
proc postGhApi(url: string, body: JsonNode) = proc postGhApi(url: string, body: JsonNode) =
checkToken() checkToken()
let client = newHttpClient() let client = newHttpClient()
@ -43,4 +65,129 @@ proc createDispatch*(workflowFileName: string, `ref`: string) =
} }
) )
type
GhArtifact = object
id: int
name: string
url: string
archive_download_url*: string
GhWorkflowRun = object
id*: int
node_id: string
run_number: int
event: string
status: string
conclusion: string
html_url: string
workflow_id: int
created_at*: string # use datetime?
updated_at: string # use datetime?
ListGhArtifactResponse = object
total_count: int
artifacts: seq[GhArtifact]
ListGhWorkflowResponse = object
total_count: int
workflow_runs: seq[GhWorkflowRun]
proc listUpdateRuns(): seq[GhWorkflowRun] =
## get update.yml runs
## endpoint https://api.github.com/repos/OWNER/REPO/actions/workflows/WORKFLOW_ID/runs
debug "listing update workflows"
let response = getGhApi("https://api.github.com/repos/daylinmorgan/oizys/actions/workflows/update.yml/runs")
fromJson(response.body, ListGhWorkflowResponse).workflow_runs
proc getLastUpdateRun*(): GhWorkflowRun =
let runs = listUpdateRuns()
let run = runs[0]
if run.conclusion == "failure":
fatalQuit bb(fmt("Most recent run was not successful\n[b]runID[/]: {run.id}\n[b]conclusion[/]: {run.conclusion}"))
if run.status in ["in_progress", "queued"]:
fatalQuit bb(fmt("Most recent run is not finished\nview workflow run at: {run.html_url}"))
result = run
proc getArtifacts(runId: int): seq[GhArtifact] =
## get workflow artifacts
## https://api.github.com/repos/OWNER/REPO/actions/runs/RUN_ID/artifacts
let response = getGhApi(fmt"https://api.github.com/repos/daylinmorgan/oizys/actions/runs/{runId}/artifacts")
fromJson(response.body, ListGhArtifactResponse).artifacts
proc getUpdateSummaryArtifact(runId: int, host: string): GhArtifact =
let name = fmt"{host}-summary"
let artifacts = getArtifacts(runId)
for artifact in artifacts:
if artifact.name == name:
return artifact
fatalQuit fmt"failed to find summary for run id: {runID}"
proc getUpdateSummaryUrl(runID: int, host: string): string =
## https://api.github.com/repos/OWNER/REPO/actions/artifacts/ARTIFACT_ID/ARCHIVE_FORMAT
let artifact = getUpdateSummaryArtifact(runID, host)
# httpclient was forwarding the Authorization headers,
# which confused Azure where the archive lives...
var response: Response
try:
let client = ghClient(maxRedirects = 0)
response = client.get(artifact.archive_download_url)
except:
errorQuit fmt("fetching summary failed:\n\n{response.headers}\n\n{response.body}")
if "location" notin response.headers.table:
errorQuit fmt("fetching summary failed:\n\n{response.headers}\n\n{response.body}")
let location = response.headers.table.getOrDefault("location", @[])
if location.len == 0: errorQuit fmt("location header missing url?")
return location[0]
proc fetchUpdateSummaryFromUrl(url: string): string =
withTmpDir:
let client = newHttpClient()
client.downloadFile(url, tmpDir / "summary.zip")
let reader = openZipArchive(tmpDir / "summary.zip")
try:
result = reader.extractFile("summary.md")
finally:
reader.close()
proc getUpdateSummary*(runId: int, host: string): string =
let url = getUpdateSummaryUrl(runId, host)
result = fetchUpdateSummaryFromUrl(url)
type
GitRepo = object
path: string
proc git(r: GitRepo, rest: varargs[string]): string =
result = "git"
result.addArgs ["-C", r.path]
result.addArgs rest
proc checkGit(code: int) =
if code != 0: fatalQuit "git had a non-zero exit status"
proc fetch(r: GitRepo) =
let code = runCmd r.git("fetch", "origin")
checkGit code
proc status(r: GitRepo) =
let (output, _, code) = runCmdCapt(r.git("status", "--porcelain"))
checkGit code
if output.len > 0:
info "unstaged commits, cowardly exiting..."
quit QuitFailure
proc rebase(r: GitRepo, `ref`: string) =
r.status()
let code = runCmd r.git("rebase", `ref`)
checkGit code
proc updateRepo*() =
let repo = GitRepo(path: getFlake())
fetch repo
rebase repo, "origin/flake-lock"

View file

@ -20,3 +20,4 @@ gitea
lock lock
code code
comma-with-db comma-with-db
nix-index-with-db

View file

@ -94,9 +94,10 @@ proc trunc(s: string, limit: int): string =
proc display(msg: string, drvs: seq[Derivation]) = proc display(msg: string, drvs: seq[Derivation]) =
echo fmt"{msg}: [bold cyan]{drvs.len()}[/]".bb echo fmt"{msg}: [bold cyan]{drvs.len()}[/]".bb
let maxLen = min(max drvs.mapIt(it.name.len), 40) if drvs.len > 0:
for drv in drvs: let maxLen = min(max drvs.mapIt(it.name.len), 40)
echo " ", drv.name.trunc(maxLen).alignLeft(maxLen), " ", drv.hash.bb("faint") for drv in drvs:
echo " ", drv.name.trunc(maxLen).alignLeft(maxLen), " ", drv.hash.bb("faint")
proc display(output: DryRunOutput) = proc display(output: DryRunOutput) =
if isDebug(): if isDebug():
@ -130,7 +131,7 @@ proc evaluateDerivations(drvs: seq[string]): Table[string, NixDerivation] =
fromJson(output, Table[string,NixDerivation]) fromJson(output, Table[string,NixDerivation])
# TODO: replace asserts in this proc # TODO: replace asserts in this proc, would be easier with results type
proc findSystemPaths(drvs: Table[string, NixDerivation]): seq[string] = proc findSystemPaths(drvs: Table[string, NixDerivation]): seq[string] =
let hosts = getHosts() let hosts = getHosts()
let systemDrvs = collect( let systemDrvs = collect(
@ -148,7 +149,8 @@ proc findSystemPaths(drvs: Table[string, NixDerivation]): seq[string] =
func isIgnored(drv: string): bool = func isIgnored(drv: string): bool =
const ignoredPackages = (slurp "ignored.txt").splitLines() const ignoredPackages = (slurp "ignored.txt").splitLines()
drv.split("-", 1)[1].replace(".drv","") in ignoredPackages let name = drv.split("-", 1)[1].replace(".drv","")
name in ignoredPackages
proc systemPathDrvsToBuild(): seq[string] = proc systemPathDrvsToBuild(): seq[string] =
let toBuild = toBuildNixosConfiguration() let toBuild = toBuildNixosConfiguration()
@ -157,11 +159,12 @@ proc systemPathDrvsToBuild(): seq[string] =
var inputDrvs: seq[string] var inputDrvs: seq[string]
for p in systemPaths: for p in systemPaths:
inputDrvs &= drvs[p].inputDrvs.keys().toSeq() inputDrvs &= drvs[p].inputDrvs.keys().toSeq()
result = collect( result = inputDrvs.filterIt(it in toBuild)
for drv in inputDrvs: let nToBuild = result.len
if (drv in toBuild) and (not drv.isIgnored()): result = result.filterIt(not it.isIgnored)
drv & "^*" let nIgnored = result.len - nToBuild
) debug fmt"ignored {nIgnored} derivations"
result = result.mapIt(it & "^*")
func splitDrv(drv: string): tuple[name, hash:string] = func splitDrv(drv: string): tuple[name, hash:string] =
let s = drv.split("-", 1) let s = drv.split("-", 1)

View file

@ -1,8 +1,4 @@
# oizys-nim todo's # oizys-nim todo's
- [x] nix commands including dry runs
- [ ] gh api commands
- [x] ci <- start with the easier one
- [ ] update
<!-- generated with <3 by daylinmorgan/todo --> <!-- generated with <3 by daylinmorgan/todo -->