diff --git a/.github/workflows/diff-pr.yml b/.github/workflows/diff-pr.yml index 17b0abf..d8fbabb 100644 --- a/.github/workflows/diff-pr.yml +++ b/.github/workflows/diff-pr.yml @@ -17,10 +17,18 @@ jobs: steps: - uses: actions/checkout@v4 with: + ref: ${{ github.event.pull_request.head.sha }} fetch-depth: 0 + - name: merge base branch + run: | + git config user.email "ci@localhost" + git config user.name "CI" + git fetch origin ${{ github.event.pull_request.base.ref }} + git merge --no-edit origin/${{ github.event.pull_request.base.ref }} + - name: worktree base for diff - run: git worktree add "$RUNNER_TEMP/base" "${{ github.event.pull_request.base.sha }}" + run: git worktree add "$RUNNER_TEMP/base" "origin/${{ github.event.pull_request.base.ref }}" - name: build closures and render diff run: | diff --git a/.github/workflows/flake-update.yml b/.github/workflows/flake-update.yml index 27ec96d..2baadce 100644 --- a/.github/workflows/flake-update.yml +++ b/.github/workflows/flake-update.yml @@ -5,21 +5,19 @@ on: - cron: "17 4 * * *" workflow_dispatch: -permissions: - contents: write - pull-requests: write - jobs: update: runs-on: nix steps: - uses: actions/checkout@v4 + with: + persist-credentials: false - run: nix flake update -L - name: open/update PR env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + GH_TOKEN: ${{ secrets.BOT_TOKEN }} API: ${{ github.api_url }} REPO: ${{ github.repository }} run: | diff --git a/ci.py b/ci.py index 61076ea..a3d8db0 100644 --- a/ci.py +++ b/ci.py @@ -8,6 +8,7 @@ import re import subprocess import subprocess_tee import sys +import urllib.error import urllib.request from collections import Counter from pathlib import Path @@ -26,6 +27,10 @@ def git(*args, check=True): return run("git", *args, check=check, capture_output=True) +class GiteaError(Exception): + pass + + def gitea(method, path, body=None): url = f'{os.environ["API"]}/repos/{os.environ["REPO"]}{path}' data = json.dumps(body).encode() if body is not None else None @@ -37,7 +42,11 @@ def gitea(method, path, body=None): with urllib.request.urlopen(req) as resp: raw = resp.read() except urllib.error.HTTPError as e: - sys.exit(f"gitea {method} {path}: {e.code} {e.reason}\n{e.read().decode()}") + try: + detail = json.loads(e.read()).get("message", e.reason) + except (json.JSONDecodeError, UnicodeDecodeError): + detail = e.reason + raise GiteaError(f"{method} {path}: {e.code} {detail}") from None return json.loads(raw) if raw else None @@ -466,16 +475,24 @@ def open_pr(args): host = os.environ["API"].removesuffix("/api/v1") auth = base64.b64encode(f'token:{os.environ["GH_TOKEN"]}'.encode()).decode() - git("-c", f"http.extraHeader=Authorization: Basic {auth}", - "push", "-f", f'{host}/{os.environ["REPO"]}.git', f"HEAD:{args.branch}") + push = git("-c", f"http.extraHeader=Authorization: Basic {auth}", + "push", "-f", f'{host}/{os.environ["REPO"]}.git', f"HEAD:{args.branch}", + check=False) + if push.returncode != 0: + raise RuntimeError(f"git push failed:\n{push.stderr}") - owner = os.environ["REPO"].split("/")[0] - if gitea("GET", f"/pulls?state=open&head={owner}:{args.branch}"): + pulls = gitea("GET", f"/pulls?state=open&base_branch={args.base}") + existing = next((pr for pr in pulls if pr["head"]["ref"] == args.branch), None) + if existing: + print(f"PR #{existing['number']} already open, pushed update") return labels = [label_id(args.label)] if args.label else [] - gitea("POST", "/pulls", - {"title": args.title, "head": args.branch, "base": args.base, - "body": Path(args.body_file).read_text(), "labels": labels}) + pr = gitea("POST", "/pulls", + {"title": args.title, "head": args.branch, "base": args.base, + "body": Path(args.body_file).read_text(), "labels": labels}) + if not pr or "number" not in pr: + raise GiteaError(f"POST /pulls: unexpected response: {json.dumps(pr)}") + print(f"opened PR #{pr['number']}") def main(): diff --git a/configuration.nix b/configuration.nix index 7461aba..da77608 100644 --- a/configuration.nix +++ b/configuration.nix @@ -5,6 +5,7 @@ hostInfo, customPkgs, flakeRev, + deployLabel, ... }: let @@ -62,14 +63,7 @@ in time.timeZone = "UTC"; system.stateVersion = "26.05"; - system.nixos.label = - let - labelFile = ./.nixos-label; - in - if builtins.pathExists labelFile then - lib.removeSuffix "\n" (builtins.readFile labelFile) - else - flakeRev; + system.nixos.label = if deployLabel != null then deployLabel else flakeRev; nix = { settings = { diff --git a/flake.lock b/flake.lock index 0c21124..6a89746 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,17 @@ { "nodes": { + "deploy-label": { + "flake": false, + "locked": { + "path": "./misc/deploy-label", + "type": "path" + }, + "original": { + "path": "./misc/deploy-label", + "type": "path" + }, + "parent": [] + }, "flake-utils": { "inputs": { "systems": "systems" @@ -47,11 +59,11 @@ ] }, "locked": { - "lastModified": 1782711974, - "narHash": "sha256-ju0/qU6ZGqRxc5yDhuljrprG0BSPgguc8Kz3txktSjI=", + "lastModified": 1790093442, + "narHash": "sha256-n0t8bN5x0NU9Ugwa/Vr0bcnA4YKA3YmyVg+Vp7t70HU=", "owner": "Floweynt", "repo": "monumenta-charm-eval", - "rev": "1aa8b532975679787f7c5bed180fd7fc0da6b905", + "rev": "0aaac697ff4d65e7af2fc9bebaf4f479d7c763b6", "type": "github" }, "original": { @@ -63,11 +75,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1789546076, - "narHash": "sha256-zVxLZiSnmaaPLwnhj7pwmqe3axBg/C6nG5JZsJMh2g4=", + "lastModified": 1790046670, + "narHash": "sha256-MYiI+CzL0tuWgRPjGsKCDHqYs2T3OzMlMQWOYWG0qso=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "b1b875982b17dabde9b4a37f3e229e74913e6db3", + "rev": "6774f7bc253789b113a4f39285dc0fa100abeacc", "type": "github" }, "original": { @@ -79,6 +91,7 @@ }, "root": { "inputs": { + "deploy-label": "deploy-label", "microvm": "microvm", "mtce": "mtce", "nixpkgs": "nixpkgs" diff --git a/flake.nix b/flake.nix index 541b7fc..95b7e43 100644 --- a/flake.nix +++ b/flake.nix @@ -7,6 +7,8 @@ mtce.inputs.nixpkgs.follows = "nixpkgs"; microvm.url = "github:microvm-nix/microvm.nix"; microvm.inputs.nixpkgs.follows = "nixpkgs"; + deploy-label.url = "path:./misc/deploy-label"; + deploy-label.flake = false; }; outputs = @@ -15,6 +17,7 @@ nixpkgs, mtce, microvm, + deploy-label, ... }: let @@ -23,6 +26,12 @@ pkgs = import nixpkgs { inherit system; }; flakeRev = self.shortRev or self.dirtyShortRev or "unknown"; + deployLabel = + let + labelFile = "${deploy-label}/label"; + raw = lib.removeSuffix "\n" (builtins.readFile labelFile); + in + if raw != "" then raw else null; customPkgs = { hopper-rs = pkgs.callPackage ./packages/hopper-rs.nix { }; @@ -56,7 +65,7 @@ specialArgsFor = hostName: { hostInfo = hostInfoFor hostName; - inherit inputs customPkgs flakeRev; + inherit inputs customPkgs flakeRev deployLabel; }; hostDefFor = diff --git a/misc/deploy-label/label b/misc/deploy-label/label new file mode 100644 index 0000000..e69de29 diff --git a/modules/autodeploy.nix b/modules/autodeploy.nix index 2d8c7d3..f49c766 100644 --- a/modules/autodeploy.nix +++ b/modules/autodeploy.nix @@ -38,7 +38,7 @@ in type = lib.types.listOf lib.types.str; default = [ ]; }; - + repoOwner = lib.mkOption { type = lib.types.str; description = "User that owns the configuration checkout"; diff --git a/scripts/server-autodeploy.sh b/scripts/server-autodeploy.sh index f2559f2..54a236c 100644 --- a/scripts/server-autodeploy.sh +++ b/scripts/server-autodeploy.sh @@ -48,13 +48,17 @@ runas git tag -v "$latest" || { sha=$(runas git rev-list -1 "$latest") report_status "pending" "deploying $latest" "$sha" +label_dir=$(mktemp -d) +echo "$latest" > "$label_dir/label" flake_url="git+${GITEA_URL}/${GITEA_REPO}?ref=refs/tags/${latest}&rev=${sha}#$HOSTNAME" -if nixos-rebuild switch --flake "$flake_url"; then +if nixos-rebuild switch --flake "$flake_url" --override-input deploy-label "path:$label_dir"; then + rm -rf "$label_dir" echo "$latest" > "$STATE_DIR/tag" report_status "success" "deployed $latest" "$sha" echo "deployed $latest" else + rm -rf "$label_dir" report_status "failure" "nixos-rebuild failed" "$sha" exit 1 fi