name: Update Flake Input description: Update one GitHub-backed Nix flake input and create a pull request inputs: input-name: description: Name of the flake input to update required: true github-token: description: Token used to query GitHub, push the update branch, and manage the pull request required: true base-branch: description: Branch targeted by the pull request required: false default: main minimum-release-age-days: description: Minimum age of the target commit in days required: false default: "3" skip-delay: description: Update to the latest revision without applying the minimum age required: false default: "false" auto-merge: description: Enable squash auto-merge on the pull request required: false default: "true" pr-labels: description: Comma-separated labels to add when they already exist in the repository required: false default: dependencies,automated outputs: updated: description: Whether flake.lock changed value: ${{ steps.update.outputs.updated }} current-version: description: Previous locked revision value: ${{ steps.update.outputs.current_version }} new-version: description: New locked revision value: ${{ steps.update.outputs.new_version }} pr-url: description: URL of the created or updated pull request value: ${{ steps.pull-request.outputs.pr_url }} runs: using: composite steps: - name: Update flake input id: update shell: bash env: GH_TOKEN: ${{ inputs.github-token }} INPUT_NAME: ${{ inputs.input-name }} MINIMUM_RELEASE_AGE_DAYS: ${{ inputs.minimum-release-age-days }} SKIP_DELAY: ${{ inputs.skip-delay }} run: | set -euo pipefail if [[ ! "$MINIMUM_RELEASE_AGE_DAYS" =~ ^[0-9]+$ ]]; then echo "::error::minimum-release-age-days must be a non-negative integer" exit 1 fi node_key="$( jq -er --arg input "$INPUT_NAME" ' .nodes.root.inputs[$input] | if type == "array" then .[0] else . end ' flake.lock )" input_type="$(jq -r --arg node "$node_key" '.nodes[$node].locked.type // ""' flake.lock)" input_owner="$(jq -r --arg node "$node_key" '.nodes[$node].locked.owner // ""' flake.lock)" input_repo="$(jq -r --arg node "$node_key" '.nodes[$node].locked.repo // ""' flake.lock)" input_ref="$(jq -r --arg node "$node_key" '.nodes[$node].original.ref // ""' flake.lock)" current_rev="$(jq -er --arg node "$node_key" '.nodes[$node].locked.rev' flake.lock)" if [ "$input_type" != "github" ] || [ -z "$input_owner" ] || [ -z "$input_repo" ]; then echo "::error::${INPUT_NAME} is not a GitHub-backed flake input" exit 1 fi echo "Input: $INPUT_NAME" echo "Repository: ${input_owner}/${input_repo}" echo "Current revision: $current_rev" if [ "$SKIP_DELAY" = "true" ]; then nix flake update "$INPUT_NAME" else cutoff="$(date --utc --date="${MINIMUM_RELEASE_AGE_DAYS} days ago" +%Y-%m-%dT%H:%M:%SZ)" api_args=( --method GET "repos/${input_owner}/${input_repo}/commits" -f "until=$cutoff" -f per_page=1 ) if [ -n "$input_ref" ]; then api_args+=(-f "sha=$input_ref") fi echo "Selecting the newest commit no later than $cutoff" target_data="$(gh api "${api_args[@]}" --jq '.[0] | {sha: .sha, date: .commit.committer.date}')" target_rev="$(jq -er '.sha' <<< "$target_data")" target_date="$(jq -er '.date' <<< "$target_data")" if [ "$target_rev" = "$current_rev" ]; then echo "The input is already at the newest eligible revision" { echo "updated=false" echo "current_version=$current_rev" echo "new_version=$current_rev" } >> "$GITHUB_OUTPUT" exit 0 fi current_date="$( gh api "repos/${input_owner}/${input_repo}/commits/${current_rev}" \ --jq '.commit.committer.date' )" current_timestamp="$(date --date="$current_date" +%s)" target_timestamp="$(date --date="$target_date" +%s)" if [ "$target_timestamp" -lt "$current_timestamp" ]; then echo "The newest eligible revision is older than the current revision; skipping" { echo "updated=false" echo "current_version=$current_rev" echo "new_version=$current_rev" } >> "$GITHUB_OUTPUT" exit 0 fi nix flake update "$INPUT_NAME" \ --override-input "$INPUT_NAME" "github:${input_owner}/${input_repo}/${target_rev}" fi if git diff --quiet -- flake.lock; then echo "No lock file changes were produced" { echo "updated=false" echo "current_version=$current_rev" echo "new_version=$current_rev" } >> "$GITHUB_OUTPUT" exit 0 fi new_node_key="$( jq -er --arg input "$INPUT_NAME" ' .nodes.root.inputs[$input] | if type == "array" then .[0] else . end ' flake.lock )" new_rev="$(jq -er --arg node "$new_node_key" '.nodes[$node].locked.rev' flake.lock)" echo "New revision: $new_rev" { echo "updated=true" echo "current_version=$current_rev" echo "new_version=$new_rev" echo "input_owner=$input_owner" echo "input_repo=$input_repo" } >> "$GITHUB_OUTPUT" - name: Create or update pull request id: pull-request if: steps.update.outputs.updated == 'true' shell: bash env: GH_TOKEN: ${{ inputs.github-token }} INPUT_NAME: ${{ inputs.input-name }} BASE_BRANCH: ${{ inputs.base-branch }} CURRENT_REV: ${{ steps.update.outputs.current_version }} NEW_REV: ${{ steps.update.outputs.new_version }} INPUT_OWNER: ${{ steps.update.outputs.input_owner }} INPUT_REPO: ${{ steps.update.outputs.input_repo }} MINIMUM_RELEASE_AGE_DAYS: ${{ inputs.minimum-release-age-days }} SKIP_DELAY: ${{ inputs.skip-delay }} AUTO_MERGE: ${{ inputs.auto-merge }} PR_LABELS: ${{ inputs.pr-labels }} run: | set -euo pipefail branch_suffix="$(tr -c 'A-Za-z0-9._-' '-' <<< "$INPUT_NAME" | sed 's/-$//')" branch="update-flake-${branch_suffix}" current_short="${CURRENT_REV:0:8}" new_short="${NEW_REV:0:8}" title="chore(nix): update ${INPUT_NAME} to ${new_short}" if [ "$SKIP_DELAY" = "true" ]; then age_note="The minimum release age check was skipped for this manually requested update." else age_note="The target commit is at least ${MINIMUM_RELEASE_AGE_DAYS} days old." fi body="$( printf '%s\n' \ "Automated update of the \`${INPUT_NAME}\` flake input." \ "" \ "- Previous revision: [\`${current_short}\`](https://github.com/${INPUT_OWNER}/${INPUT_REPO}/commit/${CURRENT_REV})" \ "- New revision: [\`${new_short}\`](https://github.com/${INPUT_OWNER}/${INPUT_REPO}/commit/${NEW_REV})" \ "- Changes: [compare](https://github.com/${INPUT_OWNER}/${INPUT_REPO}/compare/${CURRENT_REV}...${NEW_REV})" \ "" \ "$age_note" )" git config user.name "github-actions[bot]" git config user.email "41898282+github-actions[bot]@users.noreply.github.com" git add flake.lock git switch -C "$branch" git commit -m "$title" git fetch origin "refs/heads/${branch}:refs/remotes/origin/${branch}" || true git push --force-with-lease origin "HEAD:refs/heads/${branch}" label_args=() available_labels="$(gh label list --limit 100 --json name --jq '.[].name')" IFS=',' read -ra requested_labels <<< "$PR_LABELS" for label in "${requested_labels[@]}"; do label="$(xargs <<< "$label")" if [ -n "$label" ] && grep -Fxq "$label" <<< "$available_labels"; then label_args+=(--add-label "$label") elif [ -n "$label" ]; then echo "::warning::Skipping missing pull request label: $label" fi done pr_number="$( gh pr list \ --state open \ --head "$branch" \ --json number \ --jq '.[0].number // empty' )" if [ -n "$pr_number" ]; then gh pr edit "$pr_number" \ --title "$title" \ --body "$body" \ "${label_args[@]}" else gh pr create \ --base "$BASE_BRANCH" \ --head "$branch" \ --title "$title" \ --body "$body" pr_number="$( gh pr list \ --state open \ --head "$branch" \ --json number \ --jq '.[0].number' )" if [ "${#label_args[@]}" -gt 0 ]; then gh pr edit "$pr_number" "${label_args[@]}" fi fi if [ "$AUTO_MERGE" = "true" ]; then gh pr merge "$pr_number" --auto --squash || echo "::warning::Auto-merge could not be enabled; check the repository merge settings" fi pr_url="$(gh pr view "$pr_number" --json url --jq '.url')" echo "pr_url=$pr_url" >> "$GITHUB_OUTPUT" echo "Pull request: $pr_url"