Skip to content
Snippets Groups Projects
update-nixpkgs 7.81 KiB
Newer Older
#!/usr/bin/env nix-shell
#!nix-shell -i bash -p nixUnstable git curl python3

# ^^
# we get nixUnstable for the diff-closures command, mostly.
# we need git to commit and push our changes
# we need curl to create the gitlab MR
# we need python to format the data as json
main() {
    # This is a base64-encoded OpenSSH-format SSH private key that we can use
    # to push and pull with git over ssh.
    local SSHKEY=$1
    shift

    # This is a GitLab authentication token we can use to make API calls onto
    # GitLab.
    local TOKEN=$1
    # This is the URL of the root of the GitLab API.
    local SERVER_URL=$1
    shift

    # This is the hostname of the GitLab server (suitable for use in a Git
    # remote).
    local SERVER_HOST=$1
    shift

    # This is the "group/project"-style identifier for the project we're working
    # with.
    local PROJECT_PATH=$1
    shift

    # The GitLab id of the project (eg, from CI_PROJECT_ID in the CI
    # environment).
    local PROJECT_ID=$1
    # The name of the branch on which to base changes and which to target with
    # the resulting merge request.
    local DEFAULT_BRANCH=$1
    shift

    # Only proceed if we have an ssh-agent.
    check_agent

    # Pick a branch name into which to push our work.
    local SOURCE_BRANCH="nixpkgs-upgrade-$(date +%Y-%m-%d)"
    checkout_source_branch "$SSHKEY" "$SERVER_HOST" "$PROJECT_PATH" "$DEFAULT_BRANCH" "$SOURCE_BRANCH"
    build "result-before"
    # If nothing changed, report this and exit without an error.
    if ! update_nixpkgs; then
	echo "No changes."
	exit 0
    fi
    build "result-after"
    local DIFF=$(compute_diff "./result-before" "./result-after")
    commit_and_push "$SSHKEY" "$SOURCE_BRANCH" "$DIFF"
    create_merge_request "$SERVER_URL" "$TOKEN" "$PROJECT_ID" "$DEFAULT_BRANCH" "$SOURCE_BRANCH" "$DIFF"
# Add the ssh key required to push and (maybe) pull to the ssh-agent.  This
# may have a limited lifetime in the agent so operations that are going to
# require the key should refresh it immediately before starting.
refresh_ssh_key() {
    local KEY_BASE64=$1

    # A GitLab CI/CD variable set for us to use.
    echo "${KEY_BASE64}" | base64 -d | ssh-add -
}
# Make git usable by setting some global mandatory options.
setup_git() {
    # We may not know the git/ssh server's host key yet.  In that case, learn
    # it and proceed.
    export GIT_SSH_COMMAND="ssh -o StrictHostKeyChecking=accept-new"

    git config --global user.email "update-bot@private.storage"
    git config --global user.name "Update Bot"
}

# Return with an error if no ssh-agent is detected.
check_agent() {
    # We require an ssh-agent to be available so we can put the ssh private
    # key in it.  The key is given to us in memory and we don't really want to
    # put it on disk anywhere so an agent is the easiest way to make it
    # available for git/ssh operations.
    if [ ! -v SSH_AUTH_SOCK ]; then
	echo "ssh-agent is required but missing, aborting."
	exit 1
    fi
}
# Make a fresh clone of the repository, make it our working directory, and
# check out the branch we intend to commit to (the "source" of the MR).
checkout_source_branch() {
    local SSHKEY=$1
    local SERVER_HOST=$1
    local PROJECT_PATH=$1
    # The branch we'll start from.
    local DEFAULT_BRANCH=$1
    shift
    # The name of our branch.
    local BRANCH=$1
    shift

    # To avoid messing with the checkout we're running from (which GitLab
    # tends to like to share across builds) clone it to a new temporary path.
    git clone . working-copy
    cd working-copy
Update Bot's avatar
Update Bot committed

    # Make sure we know the name of a remote that points at the right place.
    # Then use it to make sure the base branch is up-to-date.  It usually
    # should be already but in case it isn't we don't want to start from a
    # stale revision.
    git remote add upstream gitlab@"$SERVER_HOST":"$PROJECT_PATH".git
    refresh_ssh_key "$SSHKEY"
    git fetch upstream "$DEFAULT_BRANCH"
    # Typically this tool runs infrequently enough that the branch doesn't
    # already exist.  However, as a convenience for developing on this tool
    # itself, if it does already exist, wipe it and start fresh for greater
    # predictability.
    git branch -D "${BRANCH}" || true

    # Then create a new branch starting from the mainline development branch.
    git checkout -B "${BRANCH}" upstream/"$DEFAULT_BRANCH"
Update Bot's avatar
Update Bot committed
# Build all of the grids (the `morph` attribute of `default.nix`) and link the
# result to the given parameter.  This will give us some material to diff.
build() {
    # The name of the nix result symlink.
    local RESULT=$1
    # The local grid can only build if you populate its users.
    echo '{}' > morph/grid/local/public-keys/users.nix
    nix-build -A morph -o "$RESULT"
}
# Perform the actual dependency update.  If there are no changes, exit with an
# error code.
update_nixpkgs() {
    # Spawn *another* nix-shell that has the *other* update-nixpkgs tool.
    # Should sort out this mess sooner rather than later...  Also, tell the
    # tool (running from another checkout) to operate on this clone's package
    # file instead of the one that's part of its own checkout.
    nix-shell ../shell.nix --run 'update-nixpkgs ${PWD}/nixpkgs.json'

    # Signal a kind of error if we did nothing (expected in the case where
    # nixpkgs hasn't changed since we last ran).
    if git diff --exit-code; then
# Return a description of the package changes resulting from the dependency
# update.
compute_diff() {
    local LEFT=$1
    local RIGHT=$1
    shift
    nix --extra-experimental-features nix-command store diff-closures "$LEFT" "$RIGHT"
}
# Commit and push all changes in the working tree along with a description of
# the package changes.
commit_and_push() {
    local SSHKEY=$1
    local BRANCH=$1
    local DIFF=$1
    shift

    git commit -am "bump nixpkgs

```
$DIFF
```
"
    refresh_ssh_key "$SSHKEY"
    git push --force upstream "${BRANCH}:${BRANCH}"
}
# Create a GitLab MR for the branch we just pushed, including a description of
# the package changes it implies.
create_merge_request() {
    local SERVER_URL=$1
    local TOKEN=$1
    local PROJECT_ID=$1
    shift
Update Bot's avatar
Update Bot committed
    # The target branch of the MR.
    local TARGET_BRANCH=$1
Update Bot's avatar
Update Bot committed
    # The source branch of the MR.
Update Bot's avatar
Update Bot committed
    local SOURCE_BRANCH=$1
    local DIFF=$1
    local BODY=$(python3 -c '
import sys, json, re
def rewrite_escapes(s):
Update Bot's avatar
Update Bot committed
    # `nix store diff-closures` output is fancy and includes color codes and
    # such.  That looks a bit less than nice in a markdown-formatted comment so
    # strip all of it.  If we wanted to be fancy we could rewrite it in a
    # markdown friendly way (eg using html).
    return re.sub(r"\x1b\[[^m]*m", "", s)
    "id": sys.argv[1],
    "target_branch": sys.argv[2],
    "source_branch": sys.argv[3],
    "title": "bump nixpkgs version",
    "description": f"```\n{rewrite_escapes(sys.argv[4])}\n```",
Update Bot's avatar
Update Bot committed
' "$PROJECT_ID" "$TARGET_BRANCH" "$SOURCE_BRANCH" "$DIFF")
    curl --verbose -X POST --data "${BODY}" --header "Content-Type: application/json" --header "PRIVATE-TOKEN: ${TOKEN}" "${SERVER_URL}/api/v4/projects/${PROJECT_ID}/merge_requests"
}

# Pull the private ssh key and GitLab token from the environment here so we
# can work with them as arguments everywhere else.  They're passed to us in
# the environment because *maybe* this is *slightly* safer than passing them
# in argv.
SSHKEY="$UPDATE_NIXPKGS_PRIVATE_SSHKEY_BASE64"
TOKEN="$UPDATE_NIXPKGS_PRIVATE_TOKEN"

# Before proceeding, remove the secrets from our environment so we don't pass
# them to child processes - none of which need them.
unset UPDATE_NIXPKGS_PRIVATE_SSHKEY_BASE64 UPDATE_NIXPKGS_PRIVATE_TOKEN
main "$SSHKEY" "$TOKEN" "$@"