diff --git a/.github/workflows/copilot-setup-steps.yml b/.github/workflows/copilot-setup-steps.yml new file mode 100644 index 00000000..cfc9ff3f --- /dev/null +++ b/.github/workflows/copilot-setup-steps.yml @@ -0,0 +1,26 @@ +--- +name: Copilot Setup Steps + +"on": + workflow_dispatch: + push: + paths: + - .github/workflows/copilot-setup-steps.yml + pull_request: + paths: + - .github/workflows/copilot-setup-steps.yml + +jobs: + copilot-setup-steps: + runs-on: ubuntu-24.04 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Install base tooling for cloud agent sessions + run: | + sudo apt-get update + sudo apt-get install -y jq make + python3 -m pip install --user yamllint==1.38.0 diff --git a/.github/workflows/docker-dependency-updater.yml b/.github/workflows/docker-dependency-updater.yml new file mode 100644 index 00000000..1ed9d16d --- /dev/null +++ b/.github/workflows/docker-dependency-updater.yml @@ -0,0 +1,200 @@ +--- +name: Docker Dependency Updater + +"on": + schedule: + - cron: "0 5 * * 1" + push: + paths: + - Dockerfile + - .github/workflows/docker-dependency-updater.yml + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + issues: write + +jobs: + schedule-copilot-session: + if: github.event_name == 'schedule' + runs-on: ubuntu-24.04 + steps: + - name: Create scheduled Copilot issue + uses: actions/github-script@v8 + with: + script: | + const owner = context.repo.owner; + const repo = context.repo.repo; + const title = "chore: scheduled dependency upgrade session"; + + const existing = await github.rest.issues.listForRepo({ + owner, + repo, + state: "open", + creator: "github-actions[bot]", + per_page: 100 + }); + + const alreadyOpen = existing.data.some((issue) => issue.title === title); + if (alreadyOpen) { + core.info("Scheduled Copilot issue is already open. Skipping."); + return; + } + + await github.rest.issues.create({ + owner, + repo, + title, + body: [ + "@copilot please run this workflow in `workflow_dispatch` mode and apply dynamic dependency upgrades.", + "", + "Scope:", + "- Check Dockerfile dependency pins and ARG versions for available updates", + "- Update versions when available", + "- Validate with the repository build/tests", + "- Open or update a PR with the changes" + ].join("\n") + }); + + update-docker-dependencies: + if: github.event_name == 'workflow_dispatch' || github.event_name == 'push' + runs-on: ubuntu-24.04 + + steps: + - name: Checkout repository + uses: actions/checkout@v6 + + - name: Update Dockerfile dependency pins + id: update + shell: bash + run: | + set -euo pipefail + + dockerfile="Dockerfile" + base_image="$(awk '$1 == "FROM" { print $2; exit }' "${dockerfile}")" + update_arg() { + local arg_name="$1" + local new_value="$2" + sed -i -E "s|^ARG ${arg_name}=.*$|ARG ${arg_name}=${new_value}|" "${dockerfile}" + } + + mapfile -t apt_packages < <( + awk ' + /apt-get install -y --no-install-recommends/ { in_block=1; next } + in_block && /&&/ { in_block=0 } + in_block { + gsub(/\\/, "", $0) + gsub(/^[[:space:]]+/, "", $0) + if ($0 ~ /^[[:alnum:].+-]+=/) { + split($0, parts, "=") + print parts[1] + } + } + ' "${dockerfile}" + ) + + if [ "${#apt_packages[@]}" -gt 0 ]; then + apt_versions="$( + docker run --rm "${base_image}" bash -s -- "${apt_packages[@]}" <<'EOF' + set -euo pipefail + apt-get update >/dev/null + for pkg in "$@"; do + version="$(apt-cache madison "${pkg}" | awk 'NR==1 {print $3}')" + if [ -n "${version}" ]; then + printf '%s=%s\n' "${pkg}" "${version}" + fi + done + EOF + )" + + while IFS='=' read -r pkg version; do + [ -z "${pkg}" ] && continue + PKG="${pkg}" VERSION="${version}" perl -0pi -e 's/\Q$ENV{PKG}=\E[0-9A-Za-z.+~:-]+/$ENV{PKG}=$ENV{VERSION}/g' "${dockerfile}" + done <<< "${apt_versions}" + fi + + while IFS='=' read -r arg_name current_value; do + [ -z "${arg_name}" ] && continue + latest_value="" + + pip_package="$( + awk -v arg="${arg_name}" ' + $0 ~ "==\\$\\{" arg "\\}" { + match($0, /[A-Za-z0-9_.-]+==\$\{[A-Za-z0-9_]+\}/) + if (RSTART > 0) { + token=substr($0, RSTART, RLENGTH) + split(token, parts, "==") + print parts[1] + exit + } + } + ' "${dockerfile}" + )" + if [ -n "${pip_package}" ]; then + latest_value="$( + curl -fsSL "https://pypi.org/pypi/${pip_package}/json" 2>/dev/null \ + | jq -r '.info.version // empty' 2>/dev/null || true + )" + fi + + if [ -z "${latest_value}" ]; then + github_repo="$( + grep -m1 -E "github\\.com/[^/]+/[^/]+/releases/download/.+\\$\\{${arg_name}\\}" "${dockerfile}" \ + | sed -nE 's#.*github\.com/([^/]+/[^/]+)/releases/download/.*#\1#p' || true + )" + if [ -n "${github_repo}" ]; then + latest_value="$( + curl -fsSL "https://api.github.com/repos/${github_repo}/releases/latest" 2>/dev/null \ + | jq -r '.tag_name // empty | ltrimstr("v")' 2>/dev/null || true + )" + fi + fi + + if [ -z "${latest_value}" ] && grep -Eq "google-cloud-sdk-(\\$\\{${arg_name}\\}|\\$${arg_name})" "${dockerfile}"; then + latest_value="$( + curl -fsSL https://dl.google.com/dl/cloudsdk/channels/rapid/components-2.json 2>/dev/null \ + | jq -r '.version // empty' 2>/dev/null || true + )" + fi + + if [ -z "${latest_value}" ] && grep -Eq "awscli-exe-linux-.*(\\$\\{${arg_name}\\}|\\$${arg_name})" "${dockerfile}"; then + latest_value="$( + curl -fsSL https://raw.githubusercontent.com/aws/aws-cli/v2/CHANGELOG.rst 2>/dev/null \ + | awk '/^[0-9]+\.[0-9]+\.[0-9]+/ { print $1; exit }' || true + )" + fi + + if [ -n "${latest_value}" ] && [ "${latest_value}" != "${current_value}" ]; then + update_arg "${arg_name}" "${latest_value}" + fi + done < <(awk '/^ARG [A-Z0-9_]+=/ { split($2, kv, "="); print kv[1] "=" kv[2] }' "${dockerfile}") + + if git diff --quiet -- "${dockerfile}"; then + echo "changed=false" >> "${GITHUB_OUTPUT}" + else + echo "changed=true" >> "${GITHUB_OUTPUT}" + fi + + - name: Validate Docker build + if: github.event_name == 'workflow_dispatch' && steps.update.outputs.changed == 'true' + run: docker build --progress=plain -t dependency-update-validation . + + - name: Create pull request + if: github.event_name == 'workflow_dispatch' && steps.update.outputs.changed == 'true' + uses: peter-evans/create-pull-request@v7 + with: + commit-message: "chore(deps): update Docker dependency pins" + title: "chore(deps): update Docker dependency pins" + body: | + ## Summary + + Automated update of Docker dependency pins in `Dockerfile`, including: + + - apt package version pins + - ARG-based tool versions discoverable from Dockerfile usage patterns + + Dependabot remains the primary updater for supported ecosystems (Docker base image and GitHub Actions). + This workflow handles Dockerfile dependency pins that Dependabot does not update. + branch: automation/docker-dependency-updates + delete-branch: true