diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 210486aceb10d7a7c4eb7c98a3ddd0afde89b70a..50a346987f40a460f38037b3cbc0a8f3a6e568b2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -14,9 +14,18 @@ # As above, but rules for running when the scheduler triggers the pipeline. .schedule_rules: &RUN_ON_SCHEDULE rules: - - if: '$CI_PIPELINE_SOURCE == "schedule"' + # There are multiple schedules so make sure this one is for us. The + # `SCHEDULE_TARGET` variable is explicitly, manually set by us in the + # schedule configuration. + - if: '$SCHEDULE_TARGET != $CI_JOB_NAME' + when: "never" - - when: "never" + # Make sure this is actually a scheduled run + - if: '$CI_PIPELINE_SOURCE != "schedule"' + when: "never" + + # Conditions look good: run. + - when: "always" stages: - "build" @@ -167,3 +176,14 @@ update-nixpkgs: "$CI_PROJECT_PATH" \ "$CI_PROJECT_ID" \ "$CI_DEFAULT_BRANCH" + +update-production: + <<: *RUN_ON_SCHEDULE + stage: "build" + script: + - | + ./ci-tools/update-production \ + "$CI_SERVER_URL" \ + "$CI_PROJECT_ID" \ + "develop" \ + "production" diff --git a/ci-tools/update-nixpkgs b/ci-tools/update-nixpkgs index 76f74a2e1848d1504af6925ae1991fdf335ad80f..d4afb21ff9689bd599d1d0fd2ca08089f8767d87 100755 --- a/ci-tools/update-nixpkgs +++ b/ci-tools/update-nixpkgs @@ -54,8 +54,11 @@ main() { checkout_source_branch "$SSHKEY" "$SERVER_HOST" "$PROJECT_PATH" "$DEFAULT_BRANCH" "$SOURCE_BRANCH" build "result-before" - # If nothing changed, update_nixpkgs will just exit for us. - update_nixpkgs + # 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") @@ -156,11 +159,10 @@ update_nixpkgs() { # file instead of the one that's part of its own checkout. nix-shell ../shell.nix --run 'update-nixpkgs ${PWD}/nixpkgs.json' - # Show us what we did - and signal a kind of error if we did nothing - # (expected in the case where nixpkgs hasn't changed since we last ran). + # 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 - echo "No changes." - exit 0 + return 1 fi } diff --git a/ci-tools/update-production b/ci-tools/update-production new file mode 100755 index 0000000000000000000000000000000000000000..7892bce0922075188e92110d1b289528b2b51b22 --- /dev/null +++ b/ci-tools/update-production @@ -0,0 +1,162 @@ +#!/usr/bin/env nix-shell +#!nix-shell -i bash -p git curl python3 + +set -eux -o pipefail + +main() { + local TOKEN=$1 + shift + local SERVER_URL=$1 + shift + local PROJECT_ID=$1 + shift + local SOURCE_BRANCH=$1 + shift + local TARGET_BRANCH=$1 + shift + + # Make sure the things we want to talk about are locally known. GitLab + # seems to prefer to know about as few refs as possible. + checkout_git_ref "$SOURCE_BRANCH" + checkout_git_ref "$TARGET_BRANCH" + + # If there have been no changes we'll just abandon this update. + if ! ensure_changes "$SOURCE_BRANCH" "$TARGET_BRANCH"; then + echo "No changes." + exit 0 + fi + + local NOTES=$(describe_update "$SOURCE_BRANCH" "$TARGET_BRANCH") + + create_merge_request "$TOKEN" "$SERVER_URL" "$PROJECT_ID" "$SOURCE_BRANCH" "$TARGET_BRANCH" "$NOTES" +} + +checkout_git_ref() { + local REF=$1 + shift + + git fetch origin "$REF" +} + +ensure_changes() { + local SOURCE_BRANCH=$1 + shift + local TARGET_BRANCH=$1 + shift + + if [ "$(git rev-parse origin/"$SOURCE_BRANCH")" = "$(git rev-parse origin/"$TARGET_BRANCH")" ]; then + return 1 + fi +} + +describe_merge_request() { + git show $rev | grep 'See merge request' | sed -e 's/See merge request //' | tr -d '[:space:]' +} + +describe_merge_requests() { + local RANGE=$1 + shift + local TARGET=$1 + shift + + # Find all of the relevant merge revisions + local onelines=$(git log --merges --first-parent -m --oneline "$RANGE" | grep "into '$TARGET'") + + # Describe each merge revision + local IFS=$'\n' + for line in $onelines; do + local rev=$(echo "$line" | cut -d ' ' -f 1) + echo -n "* " + describe_merge_request $rev + echo + done +} + +describe_update() { + local SOURCE_BRANCH=$1 + shift + local TARGET_BRANCH=$1 + shift + + # Since production production (target) should not diverge from develop + # (source) it is fine to use `..` instead of `...` in the git ranges here. + # `...` encounters problems related to discovering the merge base because + # of the way GitLab manages the git checkout on CI (I think). + + local NOTES=$(git diff origin/"$TARGET_BRANCH"..origin/"$SOURCE_BRANCH" -- DEPLOYMENT-NOTES.rst) + + # There often are no notes and that makes for boring reading so toss in a + # diffstat as well. + local DIFFSTAT=$(git diff --stat origin/"$TARGET_BRANCH"..origin/"$SOURCE_BRANCH") + + local WHEN=$(git log --max-count=1 --format='%cI' origin/"$TARGET_BRANCH") + + # Describe all of the MRs that were merged into the source branch that are + # about to be merged into the target branch. + local MR=$(describe_merge_requests origin/"$TARGET_BRANCH"..origin/"$SOURCE_BRANCH" "$SOURCE_BRANCH") + + echo "\ +Changes from $SOURCE_BRANCH since $WHEN +======================================= + +Deployment Notes +---------------- +\`\`\` +$NOTES +\`\`\` + +Included Merge Requests +----------------------- +$MR + +Diff Stat +--------- +\`\`\` +$DIFFSTAT +\`\`\` +" +} + +create_merge_request() { + local TOKEN=$1 + shift + local SERVER_URL=$1 + shift + local PROJECT_ID=$1 + shift + # THe source branch of the MR. + local SOURCE_BRANCH=$1 + shift + # The target branch of the MR. + local TARGET_BRANCH=$1 + shift + local NOTES=$1 + shift + + local BODY=$(python3 -c ' +import sys, json +print(json.dumps({ + "id": sys.argv[1], + "source_branch": sys.argv[2], + "target_branch": sys.argv[3], + "remove_source_branch": True, + "title": f"update {sys.argv[3]}", + "description": sys.argv[4], +})) +' "$PROJECT_ID" "$SOURCE_BRANCH" "$TARGET_BRANCH" "$NOTES") + + 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 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. +# +# The name is slightly weird because it is shared with the update-nixpkgs job. +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_TOKEN + +main "$TOKEN" "$@"