Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ version: "2"
run:
timeout: 5m
tests: true
go: "1.25.10"
go: "1.25.11"
linters:
enable:
- dupl
Expand Down
2 changes: 1 addition & 1 deletion .mise.toml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tools]
go = "1.25.10"
go = "1.25.11"
node = "22.22.0"
pnpm = "11.0.9"
python = "3.12.12"
Expand Down
2 changes: 2 additions & 0 deletions .typos.toml
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@
optin = 'optin'
fa-adn = 'fa-adn'
ERRO = 'ERRO'
fter = "fter"
etry = "etry"
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
FROM esacteksab/go:1.25.10-2026-05-22@sha256:1eb57ae5ddbfbd05da2a4ea91cf778516b696a7098b63f2890438fc298905e1a AS builder
FROM esacteksab/go:1.25.11-2026-06-12@sha256:76db7b11120372912b1e2c92d6339cf9b5f82ea43f554955d3954fb67a142c8b AS builder

# Set GOMODCACHE explicitly (still good practice)
ENV GOMODCACHE=/go/pkg/mod
Expand Down
4 changes: 2 additions & 2 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -131,8 +131,8 @@ update:
.PHONY: update-go-version
update-go-version:
@if [ -z "$(or $(GO_VERSION),$(version))" ]; then \
echo "Usage: make update-go-version GO_VERSION=1.25.10"; \
echo " or: make update-go-version version=1.25.10"; \
echo "Usage: make update-go-version GO_VERSION=1.25.11"; \
echo " or: make update-go-version version=1.25.11"; \
exit 1; \
fi
./scripts/update-go-version.sh "$(or $(GO_VERSION),$(version))"
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
module github.com/esacteksab/gh-tp

go 1.25.10
go 1.25.11

require (
github.com/MakeNowJust/heredoc v1.0.0
Expand Down
2 changes: 1 addition & 1 deletion go.tool.mod
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
// How to use https://www.alexedwards.net/blog/how-to-manage-tool-dependencies-in-go-1.24-plus
module github.com/esacteksab/gh-tp

go 1.25.10
go 1.25.11

tool (
github.com/segmentio/golines
Expand Down
196 changes: 169 additions & 27 deletions scripts/update-go-version.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,21 @@ set -euo pipefail
readonly VERSION_INPUT="${1:-}"
readonly DOCKERFILE="Dockerfile"

# Global initialization (Token managed internally; no longer leaks via set -x)
HUB_TOKEN=""

# Manage temporary files robustly, even if the script crashes or dies early.
declare -a CLEANUP_FILES=()
cleanup() {
if [[ ${#CLEANUP_FILES[@]} -gt 0 ]]; then
rm -f "${CLEANUP_FILES[@]}"
fi
}
trap cleanup EXIT

# FIX: Redirect to stderr (>&2) so command substitutions $(...) don't capture log output
log() {
echo "[update-go-version] $*"
echo "[update-go-version] $*" >&2
}

die() {
Expand Down Expand Up @@ -37,6 +50,89 @@ replace_or_fail() {
sed -E -i "$sed_expr" "$file"
}

# ---------------------------------------------------------------------------
# Docker Hub API helpers
# ---------------------------------------------------------------------------

hub_authenticate() {
if [[ -n "${DOCKERHUB_USERNAME:-}" && -n "${DOCKERHUB_TOKEN:-}" ]]; then
log "Authenticating to Docker Hub as ${DOCKERHUB_USERNAME}"
local resp

# Send credentials safely, suppressing stdout.
# If it fails, `|| true` catches it, and jq handles the empty/invalid output safely.
resp="$(curl -sS \
-H 'Content-Type: application/json' \
-d "{\"username\": \"${DOCKERHUB_USERNAME}\", \"password\": \"${DOCKERHUB_TOKEN}\"}" \
"https://hub.docker.com/v2/users/login" 2>/dev/null || true)"

HUB_TOKEN="$(printf '%s' "$resp" | jq -r '.token // empty')"
[[ -n "$HUB_TOKEN" ]] || die "Docker Hub authentication failed (check credentials)."
log "Docker Hub authentication succeeded"
else
log "No DOCKERHUB_USERNAME/DOCKERHUB_TOKEN set; using anonymous Hub API access"
fi
}

hub_get() {
local url="$1"
local attempt=1
local max_attempts="${HUB_MAX_ATTEMPTS:-6}"
local delay=2

while :; do
local body_file
body_file="$(mktemp)"
CLEANUP_FILES+=("$body_file")

local headers_file
headers_file="$(mktemp)"
CLEANUP_FILES+=("$headers_file")

# Use arrays to prevent Authorization tokens from leaking into `ps aux` process tables
local curl_args=(
-sS
-o "$body_file"
-D "$headers_file"
-w '%{http_code}'
-H 'Accept: application/json'
)

if [[ -n "${HUB_TOKEN}" ]]; then
curl_args+=( -H "Authorization: Bearer ${HUB_TOKEN}" )
fi

local http
http="$(curl "${curl_args[@]}" "$url" 2>/dev/null || echo "000")"

if [[ "$http" == "200" ]]; then
cat "$body_file"
return 0
fi

if { [[ "$http" == "429" ]] || [[ "$http" == 5* ]] || [[ "$http" == "000" ]]; } && (( attempt < max_attempts )); then
local retry_after
retry_after="$(grep -i '^Retry-After:' "$headers_file" 2>/dev/null | tail -n1 \
| sed -E 's/^[Rr]etry-[Aa]fter:[[:space:]]*([0-9]+).*/\1/' | tr -d '\r')"

local sleep_for
if [[ "$retry_after" =~ ^[0-9]+$ ]]; then
sleep_for="$retry_after"
else
sleep_for="$delay"
fi

log "Docker Hub API returned HTTP ${http}; retry ${attempt}/${max_attempts} after ${sleep_for}s"
sleep "$sleep_for"
(( delay = delay * 2 > 60 ? 60 : delay * 2 ))
(( attempt++ ))
continue
fi

die "Docker Hub request failed (HTTP ${http}): ${url}"
done
}

update_known_version_files() {
log "Updating Go version references in known files"

Expand Down Expand Up @@ -64,7 +160,7 @@ update_known_version_files() {
}
END {
if (!updated) {
print "missing go/golang entry in [tools] section of .mise.toml; add one like: [tools] go = \"" version "\"" > "/dev/stderr"
print "missing go/golang entry in [tools] section of .mise.toml" > "/dev/stderr"
exit 1
}
}
Expand Down Expand Up @@ -104,22 +200,28 @@ find_latest_dated_tag() {

local namespace="${ns_repo%/*}"
local repository="${ns_repo#*/}"
local api_url="https://hub.docker.com/v2/namespaces/${namespace}/repositories/${repository}/tags?page_size=100"

local api_url="https://hub.docker.com/v2/namespaces/${namespace}/repositories/${repository}/tags?page_size=100&name=${VERSION_INPUT}-"
local matches=""

# Ensure regex literal dots are escaped for jq's test()
local safe_version="${VERSION_INPUT//./\\.}"

while [[ -n "$api_url" && "$api_url" != "null" ]]; do
local body
body="$(curl -fsSL "$api_url")"
body="$(hub_get "$api_url")"

local page
page="$(printf '%s\n' "$body" | grep -oE '"name"[[:space:]]*:[[:space:]]*"[^"]+"' | sed -E 's/^"name"[[:space:]]*:[[:space:]]*"([^"]+)"$/\1/' | grep -E "^${VERSION_INPUT}-[0-9]{4}-[0-9]{2}-[0-9]{2}$" || true)"
page="$(printf '%s\n' "$body" | jq -r --arg regex "^${safe_version}-[0-9]{4}-[0-9]{2}-[0-9]{2}$" '
.results[].name | select(. != null and test($regex))
')"

if [[ -n "$page" ]]; then
matches+=$'\n'
matches+="$page"
fi

api_url="$(printf '%s\n' "$body" | tr -d '\n' | sed -nE 's/.*"next"[[:space:]]*:[[:space:]]*"([^"]*)".*/\1/p')"
[[ -n "$api_url" ]] || api_url="null"
api_url="$(printf '%s\n' "$body" | jq -r '.next // empty')"
done

local latest
Expand All @@ -132,40 +234,75 @@ resolve_digest_with_docker() {
local image_ref="$1"
local digest=""

local err_file
err_file="$(mktemp)"
CLEANUP_FILES+=("$err_file")

# Enforce Docker Content Trust to mitigate image spoofing/tampering.
export DOCKER_CONTENT_TRUST="${DOCKER_CONTENT_TRUST:-1}"

local inspect_output
inspect_output="$(docker buildx imagetools inspect "$image_ref" 2>/dev/null || true)"
if [[ "$inspect_output" =~ Digest:[[:space:]]*(sha256:[a-f0-9]{64}) ]]; then
digest="${BASH_REMATCH[1]}"
if inspect_output="$(docker buildx imagetools inspect "$image_ref" 2>"$err_file")"; then
if [[ "$inspect_output" =~ Digest:[[:space:]]*(sha256:[a-f0-9]{64}) ]]; then
digest="${BASH_REMATCH[1]}"
fi
else
log "Notice: imagetools inspect failed (stderr: $(cat "$err_file")). Falling back to docker pull."
fi

if [[ -z "$digest" ]]; then
docker pull "$image_ref" >/dev/null 2>&1 || true
local repo_digest
repo_digest="$(docker image inspect --format '{{index .RepoDigests 0}}' "$image_ref" 2>/dev/null || true)"
if [[ "$repo_digest" == *@sha256:* ]]; then
digest="${repo_digest##*@}"
if docker pull "$image_ref" >/dev/null 2>"$err_file"; then
local repo_digest
repo_digest="$(docker image inspect --format '{{index .RepoDigests 0}}' "$image_ref" 2>/dev/null || true)"
if [[ "$repo_digest" == *@sha256:* ]]; then
digest="${repo_digest##*@}"
fi
else
log "Notice: docker pull failed (stderr: $(cat "$err_file"))."
fi
fi

[[ "$digest" =~ ^sha256:[a-f0-9]{64}$ ]] || die "Failed to resolve digest for $image_ref via docker"
printf '%s\n' "$digest"
}

update_dockerfile_base_image() {
# Constructs and returns the fully assembled replacement string,
# preventing the need for global state variables.
resolve_base_image() {
local parsed
parsed="$(extract_repo_and_stage_from_dockerfile)"

local repo="${parsed%%$'\t'*}"
local stage_suffix="${parsed#*$'\t'}"

local tag
tag="$(find_latest_dated_tag "$repo")"

local image_ref="${repo}:${tag}"
log "Resolving digest for $image_ref"

local digest
digest="$(resolve_digest_with_docker "$image_ref")"

local replacement="FROM ${image_ref}@${digest}${stage_suffix}"
awk -v line="$replacement" 'BEGIN { done = 0 } { if (!done && $0 ~ /^FROM[[:space:]]+/) { print line; done = 1 } else { print } } END { if (!done) { exit 1 } }' "$DOCKERFILE" > "$DOCKERFILE.tmp" && mv "$DOCKERFILE.tmp" "$DOCKERFILE"
printf 'FROM %s@%s%s\n' "$image_ref" "$digest" "$stage_suffix"
}

update_dockerfile_base_image() {
local replacement_line="$1"

awk -v line="$replacement_line" '
BEGIN { done = 0 }
{
if (!done && $0 ~ /^FROM[[:space:]]+/) {
print line
done = 1
} else {
print
}
}
END {
if (!done) { exit 1 }
}' "$DOCKERFILE" > "$DOCKERFILE.tmp" && mv "$DOCKERFILE.tmp" "$DOCKERFILE"
}

show_detected_go_version_refs() {
Expand All @@ -178,29 +315,34 @@ show_detected_go_version_refs() {

main() {
validate_version

# Ensure file availability
require_file "go.mod"
require_file "go.tool.mod"
require_file ".golangci.yaml"
require_file ".mise.toml"
require_file "$DOCKERFILE"

# Ensure tool availability
require_cmd grep
require_cmd sed
require_cmd awk
require_cmd curl
require_cmd docker
require_cmd jq
require_cmd mktemp

# Preflight Docker resolution before mutating files.
local preflight
preflight="$(extract_repo_and_stage_from_dockerfile)"
local preflight_repo="${preflight%%$'\t'*}"
local preflight_tag
preflight_tag="$(find_latest_dated_tag "$preflight_repo")"
resolve_digest_with_docker "${preflight_repo}:${preflight_tag}" >/dev/null
hub_authenticate

# Generate the final valid line directly (avoids global variables)
local dockerfile_replacement_line
dockerfile_replacement_line="$(resolve_base_image)"

show_detected_go_version_refs
update_known_version_files
update_dockerfile_base_image
update_dockerfile_base_image "$dockerfile_replacement_line"

log "Done. Updated Go references to $VERSION_INPUT"
}

main
main "$@"
Loading