Compare commits

..

3 Commits

Author SHA1 Message Date
faizanahmad055
9a49672fb1 Merge branch 'master' of github.com:stakater/Reloader into feature/use-SHA512 2026-03-13 15:10:00 +01:00
Muhammad Safwan Karim
81a7e7ef02 Generated release automation script (#1104)
* Generated release automation script

* release

* ignore plans directory

* delete plan
2026-03-13 07:08:18 +01:00
faizanahmad055
c9fb423f8d Use SHA512 instead of SHA1
Signed-off-by: faizanahmad055 <faizan.ahmad55@outlook.com>
2026-01-12 19:33:26 +01:00
11 changed files with 279 additions and 34 deletions

View File

@@ -10,6 +10,7 @@ on:
- 'Dockerfile-docs'
- 'docs-nginx.conf'
- 'docs/**'
- '!docs/plans/**'
- 'theme_common'
- 'theme_override'
- 'deployments/kubernetes/chart/reloader/README.md'

View File

@@ -1 +1 @@
1.4.14-alpha
1.4.14

View File

@@ -17,7 +17,7 @@ spec:
app: reloader-reloader
spec:
containers:
- image: "ghcr.io/stakater/reloader:v1.4.14-alpha"
- image: "ghcr.io/stakater/reloader:v1.4.14"
imagePullPolicy: IfNotPresent
name: reloader-reloader
env:

View File

@@ -141,7 +141,7 @@ spec:
fieldPath: metadata.namespace
- name: RELOADER_DEPLOYMENT_NAME
value: reloader-reloader
image: ghcr.io/stakater/reloader:v1.4.14-alpha
image: ghcr.io/stakater/reloader:v1.4.14
imagePullPolicy: IfNotPresent
livenessProbe:
failureThreshold: 5

View File

@@ -76,7 +76,7 @@ Note: Rolling upgrade also works in the same way for secrets.
### Hash Value Computation
Reloader uses SHA1 to compute hash value. SHA1 is used because it is efficient and less prone to collision.
Reloader uses SHA512 to compute hash value. SHA512 is used because it is efficient and less prone to collision.
## Monitor All Namespaces
@@ -90,4 +90,4 @@ The output file can then be used to deploy Reloader in specific namespace.
## Compatibility With Helm Install and Upgrade
Reloader has no impact on helm deployment cycle. Reloader only injects an environment variable in `deployment`, `daemonset` or `statefulset`. The environment variable contains the SHA1 value of `ConfigMaps` or `Secrets` data. So if a deployment is created using Helm and Reloader updates the deployment, then next time you upgrade the helm release, Reloader will do nothing except changing that environment variable value in `deployment` , `daemonset` or `statefulset`.
Reloader has no impact on helm deployment cycle. Reloader only injects an environment variable in `deployment`, `daemonset` or `statefulset`. The environment variable contains the SHA512 value of `ConfigMaps` or `Secrets` data. So if a deployment is created using Helm and Reloader updates the deployment, then next time you upgrade the helm release, Reloader will do nothing except changing that environment variable value in `deployment` , `daemonset` or `statefulset`.

View File

@@ -2,10 +2,10 @@
Reloader is inspired from [`configmapcontroller`](https://github.com/fabric8io/configmapcontroller) but there are many ways in which it differs from `configmapcontroller`. Below is the small comparison between these two controllers.
| Reloader | ConfigMap |
|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Reloader can watch both `Secrets` and `ConfigMaps`. | `configmapcontroller` can only watch changes in `ConfigMaps`. It cannot detect changes in other resources like `Secrets`. |
| Reloader can perform rolling upgrades on `deployments` as well as on `statefulsets` and `daemonsets` | `configmapcontroller` can only perform rolling upgrades on `deployments`. It currently does not support rolling upgrades on `statefulsets` and `daemonsets` |
| Reloader provides both unit test cases and end to end integration test cases for future updates. So one can make sure that new changes do not break any old functionality. | Currently there are not any unit test cases or end to end integration test cases in `configmap-controller`. It adds difficulties for any additional updates in `configmap-controller` and one can not know for sure whether new changes breaks any old functionality or not. |
| Reloader uses SHA1 to encode the change in `ConfigMap` or `Secret`. It then saves the SHA1 value in `STAKATER_FOO_CONFIGMAP` or `STAKATER_FOO_SECRET` environment variable depending upon where the change has happened. The use of SHA1 provides a concise 40 characters encoded value that is very less prone to collision. | `configmap-controller` uses `FABRICB_FOO_REVISION` environment variable to store any change in `ConfigMap` controller. It does not encode it or convert it in suitable hash value to avoid data pollution in deployment. |
| Reloader allows you to customize your own annotation (for both `Secrets` and `ConfigMaps`) using command line flags | `configmap-controller` restricts you to only their provided annotation |
| Reloader | ConfigMap |
|---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| Reloader can watch both `Secrets` and `ConfigMaps`. | `configmapcontroller` can only watch changes in `ConfigMaps`. It cannot detect changes in other resources like `Secrets`. |
| Reloader can perform rolling upgrades on `deployments` as well as on `statefulsets` and `daemonsets` | `configmapcontroller` can only perform rolling upgrades on `deployments`. It currently does not support rolling upgrades on `statefulsets` and `daemonsets` |
| Reloader provides both unit test cases and end to end integration test cases for future updates. So one can make sure that new changes do not break any old functionality. | Currently there are not any unit test cases or end to end integration test cases in `configmap-controller`. It adds difficulties for any additional updates in `configmap-controller` and one can not know for sure whether new changes breaks any old functionality or not. |
| Reloader uses SHA512 to encode the change in `ConfigMap` or `Secret`. It then saves the SHA1 value in `STAKATER_FOO_CONFIGMAP` or `STAKATER_FOO_SECRET` environment variable depending upon where the change has happened. The use of SHA1 provides a concise 40 characters encoded value that is very less prone to collision. | `configmap-controller` uses `FABRICB_FOO_REVISION` environment variable to store any change in `ConfigMap` controller. It does not encode it or convert it in suitable hash value to avoid data pollution in deployment. |
| Reloader allows you to customize your own annotation (for both `Secrets` and `ConfigMaps`) using command line flags | `configmap-controller` restricts you to only their provided annotation |

View File

@@ -6,7 +6,7 @@ Reloader and k8s-trigger-controller are both built for same purpose. So there ar
- Both controllers support change detection in `ConfigMaps` and `Secrets`
- Both controllers support deployment `rollout`
- Reloader controller use SHA1 for hashing
- Reloader controller use SHA512 for hashing
- Both controllers have end to end as well as unit test cases.
## Differences

View File

@@ -1,20 +1,12 @@
package crypto
import (
"crypto/sha1"
"fmt"
"io"
"github.com/sirupsen/logrus"
"crypto/sha512"
"encoding/hex"
)
// GenerateSHA generates SHA from string
func GenerateSHA(data string) string {
hasher := sha1.New()
_, err := io.WriteString(hasher, data)
if err != nil {
logrus.Errorf("Unable to write data in hash writer %v", err)
}
sha := hasher.Sum(nil)
return fmt.Sprintf("%x", sha)
hash := sha512.Sum512_256([]byte(data))
return hex.EncodeToString(hash[:])
}

View File

@@ -7,7 +7,7 @@ import (
// TestGenerateSHA generates the sha from given data and verifies whether it is correct or not
func TestGenerateSHA(t *testing.T) {
data := "www.stakater.com"
sha := "abd4ed82fb04548388a6cf3c339fd9dc84d275df"
sha := "2e9aa975331b22861b4f62b7fcc69b63e001f938361fee3b4ed888adf26a10e3"
result := GenerateSHA(data)
if result != sha {
t.Errorf("Failed to generate SHA")
@@ -18,11 +18,11 @@ func TestGenerateSHA(t *testing.T) {
// This ensures consistent behavior and avoids issues with string matching operations
func TestGenerateSHAEmptyString(t *testing.T) {
result := GenerateSHA("")
expected := "da39a3ee5e6b4b0d3255bfef95601890afd80709"
expected := "c672b8d1ef56ed28ab87c3622c5114069bdd3ad7b8f9737498d0c01ecef0967a"
if result != expected {
t.Errorf("Failed to generate SHA for empty string. Expected: %s, Got: %s", expected, result)
}
if len(result) != 40 {
if len(result) != 64 {
t.Errorf("SHA hash should be 40 characters long, got %d", len(result))
}
}

View File

@@ -1981,7 +1981,7 @@ func TestRollingUpgradeForDeploymentWithPatchAndRetryUsingArs(t *testing.T) {
assert.Equal(t, patchtypes.StrategicMergePatchType, patchType)
assert.NotEmpty(t, bytes)
assert.Contains(t, string(bytes), `{"spec":{"template":{"metadata":{"annotations":{"reloader.stakater.com/last-reloaded-from":`)
assert.Contains(t, string(bytes), `\"hash\":\"3c9a892aeaedc759abc3df9884a37b8be5680382\"`)
assert.Contains(t, string(bytes), `\"hash\":\"fd9e71a362056bfa864d9859e12978f893d330ce8cbf09218b25d015770ad91f\"`)
return nil
}
@@ -2964,7 +2964,7 @@ func TestRollingUpgradeForDaemonSetWithPatchAndRetryUsingArs(t *testing.T) {
assert.Equal(t, patchtypes.StrategicMergePatchType, patchType)
assert.NotEmpty(t, bytes)
assert.Contains(t, string(bytes), `{"spec":{"template":{"metadata":{"annotations":{"reloader.stakater.com/last-reloaded-from":`)
assert.Contains(t, string(bytes), `\"hash\":\"314a2269170750a974d79f02b5b9ee517de7f280\"`)
assert.Contains(t, string(bytes), `\"hash\":\"43bf9e30e7c4e32a8f8673c462b86d0b1ac626cf498afdc0d0108e79ebe7ee0c\"`)
return nil
}
@@ -3227,7 +3227,7 @@ func TestRollingUpgradeForStatefulSetWithPatchAndRetryUsingArs(t *testing.T) {
assert.Equal(t, patchtypes.StrategicMergePatchType, patchType)
assert.NotEmpty(t, bytes)
assert.Contains(t, string(bytes), `{"spec":{"template":{"metadata":{"annotations":{"reloader.stakater.com/last-reloaded-from":`)
assert.Contains(t, string(bytes), `\"hash\":\"f821414d40d8815fb330763f74a4ff7ab651d4fa\"`)
assert.Contains(t, string(bytes), `\"hash\":\"6aa837180bdf6a93306c71a0cf62b4a45c2d5b021578247b3b64d5baea2b84d9\"`)
return nil
}
@@ -3607,7 +3607,7 @@ func TestRollingUpgradeForDeploymentWithPatchAndRetryUsingErs(t *testing.T) {
assert.Equal(t, patchtypes.StrategicMergePatchType, patchType)
assert.NotEmpty(t, bytes)
assert.Contains(t, string(bytes), `{"spec":{"template":{"spec":{"containers":[{"name":`)
assert.Contains(t, string(bytes), `"value":"3c9a892aeaedc759abc3df9884a37b8be5680382"`)
assert.Contains(t, string(bytes), `"value":"fd9e71a362056bfa864d9859e12978f893d330ce8cbf09218b25d015770ad91f"`)
return nil
}
@@ -4502,7 +4502,7 @@ func TestRollingUpgradeForDaemonSetWithPatchAndRetryUsingErs(t *testing.T) {
assert.Equal(t, patchtypes.StrategicMergePatchType, patchType)
assert.NotEmpty(t, bytes)
assert.Contains(t, string(bytes), `{"spec":{"template":{"spec":{"containers":[{"name":`)
assert.Contains(t, string(bytes), `"value":"314a2269170750a974d79f02b5b9ee517de7f280"`)
assert.Contains(t, string(bytes), `"value":"43bf9e30e7c4e32a8f8673c462b86d0b1ac626cf498afdc0d0108e79ebe7ee0c"`)
return nil
}
@@ -4737,7 +4737,7 @@ func TestRollingUpgradeForStatefulSetWithPatchAndRetryUsingErs(t *testing.T) {
assert.Equal(t, patchtypes.StrategicMergePatchType, patchType)
assert.NotEmpty(t, bytes)
assert.Contains(t, string(bytes), `{"spec":{"template":{"spec":{"containers":[{"name":`)
assert.Contains(t, string(bytes), `"value":"f821414d40d8815fb330763f74a4ff7ab651d4fa"`)
assert.Contains(t, string(bytes), `"value":"6aa837180bdf6a93306c71a0cf62b4a45c2d5b021578247b3b64d5baea2b84d9"`)
return nil
}

252
scripts/release.sh Executable file
View File

@@ -0,0 +1,252 @@
#!/usr/bin/env bash
set -euo pipefail
REPO="stakater/Reloader"
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"
# Colors
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
warn() { echo -e "${YELLOW}[WARN]${NC} $*"; }
error() { echo -e "${RED}[ERROR]${NC} $*" >&2; }
confirm() {
local msg="$1"
echo -en "${YELLOW}$msg [y/N]:${NC} "
read -r answer
[[ "$answer" =~ ^[Yy]$ ]]
}
usage() {
cat <<EOF
Usage: $0 <APP_VERSION> <CHART_VERSION>
Automates the full Reloader release process.
Arguments:
APP_VERSION Application version without 'v' prefix (e.g. 1.5.0, 1.5.0-alpha)
CHART_VERSION Helm chart version (e.g. 2.3.0, 2.3.0-rc.1)
Prerequisites:
- gh CLI authenticated with repo access
- git configured with push access to $REPO
Example:
$0 1.5.0 2.3.0
EOF
exit 1
}
# --- Input validation ---
[[ $# -ne 2 ]] && usage
APP_VERSION="$1"
CHART_VERSION="$2"
# Strip 'v' prefix if provided
APP_VERSION="${APP_VERSION#v}"
CHART_VERSION="${CHART_VERSION#v}"
# Validate semver format (with optional prerelease suffix e.g. 1.5.0-alpha, 1.5.0-rc.1)
SEMVER_RE='^[0-9]+\.[0-9]+\.[0-9]+([-][a-zA-Z0-9.]+)?$'
if ! [[ "$APP_VERSION" =~ $SEMVER_RE ]]; then
error "APP_VERSION '$APP_VERSION' is not valid semver (expected X.Y.Z or X.Y.Z-prerelease)"
exit 1
fi
if ! [[ "$CHART_VERSION" =~ $SEMVER_RE ]]; then
error "CHART_VERSION '$CHART_VERSION' is not valid semver (expected X.Y.Z or X.Y.Z-prerelease)"
exit 1
fi
# Check prerequisites
if ! command -v gh &> /dev/null; then
error "gh CLI is not installed. Install from https://cli.github.com/"
exit 1
fi
if ! gh auth status &> /dev/null; then
error "gh CLI is not authenticated. Run 'gh auth login' first."
exit 1
fi
RELEASE_BRANCH="release-v${APP_VERSION}"
TAG="v${APP_VERSION}"
info "Release plan:"
info " App version: $APP_VERSION (tag: $TAG)"
info " Chart version: $CHART_VERSION"
info " Release branch: $RELEASE_BRANCH"
echo ""
# =============================================================================
# Phase 1: Create release branch
# =============================================================================
info "Phase 1: Create release branch '$RELEASE_BRANCH' from master"
if git ls-remote --heads origin "$RELEASE_BRANCH" | grep -q "$RELEASE_BRANCH"; then
warn "Branch '$RELEASE_BRANCH' already exists on remote."
if ! confirm "Continue using existing branch?"; then
error "Aborted."
exit 1
fi
else
if ! confirm "Create and push branch '$RELEASE_BRANCH' from master?"; then
error "Aborted."
exit 1
fi
git fetch origin master
git push origin origin/master:refs/heads/"$RELEASE_BRANCH"
info "Branch '$RELEASE_BRANCH' created and pushed."
fi
echo ""
# =============================================================================
# Phase 2: Trigger Init Release workflow and merge its PR
# =============================================================================
info "Phase 2: Trigger Init Release workflow"
if ! confirm "Trigger 'Init Release' workflow for branch '$RELEASE_BRANCH' with version '$APP_VERSION'?"; then
error "Aborted."
exit 1
fi
gh workflow run init-branch-release.yaml \
--repo "$REPO" \
-f TARGET_BRANCH="$RELEASE_BRANCH" \
-f TARGET_VERSION="$APP_VERSION"
info "Workflow triggered. Waiting for version bump PR to be created..."
# Poll for the PR (created by the workflow targeting the release branch)
MAX_ATTEMPTS=30
SLEEP_INTERVAL=10
PR_NUMBER=""
for i in $(seq 1 $MAX_ATTEMPTS); do
PR_NUMBER=$(gh pr list \
--repo "$REPO" \
--base "$RELEASE_BRANCH" \
--search "Bump version to $APP_VERSION" \
--json number \
--jq '.[0].number // empty' 2>/dev/null || true)
if [[ -n "$PR_NUMBER" ]]; then
info "Found PR #$PR_NUMBER"
break
fi
echo -n "."
sleep "$SLEEP_INTERVAL"
done
if [[ -z "$PR_NUMBER" ]]; then
error "Timed out waiting for Init Release PR. Check workflow status at:"
error " https://github.com/$REPO/actions/workflows/init-branch-release.yaml"
exit 1
fi
info "PR: https://github.com/$REPO/pull/$PR_NUMBER"
if ! confirm "Merge PR #$PR_NUMBER (version bump to $APP_VERSION)?"; then
error "Aborted. PR is still open: https://github.com/$REPO/pull/$PR_NUMBER"
exit 1
fi
gh pr merge "$PR_NUMBER" --repo "$REPO" --merge
info "PR #$PR_NUMBER merged."
echo ""
# =============================================================================
# Phase 3: Create GitHub release
# =============================================================================
info "Phase 3: Create GitHub release '$TAG' targeting '$RELEASE_BRANCH'"
info "This will trigger the release workflow (Docker image builds, GoReleaser)."
if ! confirm "Create GitHub release '$TAG'?"; then
error "Aborted."
exit 1
fi
gh release create "$TAG" \
--repo "$REPO" \
--target "$RELEASE_BRANCH" \
--title "Release $TAG" \
--generate-notes
info "GitHub release created: https://github.com/$REPO/releases/tag/$TAG"
info "Release workflow will run in the background."
echo ""
# =============================================================================
# Phase 4: Bump Helm chart and create PR
# =============================================================================
info "Phase 4: Bump Helm chart version to $CHART_VERSION (appVersion: v$APP_VERSION)"
HELM_BRANCH="release-helm-chart-v${CHART_VERSION}"
if ! confirm "Create branch '$HELM_BRANCH', bump chart files, and open PR with 'release/helm-chart' label?"; then
error "Aborted."
exit 1
fi
# Create branch from latest master
git fetch origin master
git checkout -b "$HELM_BRANCH" origin/master
# Bump Chart.yaml: version and appVersion
CHART_FILE="deployments/kubernetes/chart/reloader/Chart.yaml"
sed -i "s/^version:.*/version: ${CHART_VERSION}/" "$CHART_FILE"
sed -i "s/^appVersion:.*/appVersion: v${APP_VERSION}/" "$CHART_FILE"
# Bump values.yaml: image.tag
VALUES_FILE="deployments/kubernetes/chart/reloader/values.yaml"
sed -i "s/^\( tag:\).*/\1 v${APP_VERSION}/" "$VALUES_FILE"
# Show changes for review
info "Changes:"
git diff
git add "$CHART_FILE" "$VALUES_FILE"
git commit -m "Bump helm chart to ${CHART_VERSION} and appVersion to v${APP_VERSION}"
git push origin "$HELM_BRANCH"
HELM_PR_URL=$(gh pr create \
--repo "$REPO" \
--base master \
--head "$HELM_BRANCH" \
--title "Bump Helm chart to ${CHART_VERSION} (appVersion v${APP_VERSION})" \
--body "Bump Helm chart version to ${CHART_VERSION} and appVersion to v${APP_VERSION}." \
--label "release/helm-chart")
HELM_PR_NUMBER=$(echo "$HELM_PR_URL" | grep -o '[0-9]*$')
info "Helm chart PR created: $HELM_PR_URL"
if ! confirm "Merge Helm chart PR #$HELM_PR_NUMBER?"; then
error "Aborted. PR is still open: $HELM_PR_URL"
exit 1
fi
gh pr merge "$HELM_PR_NUMBER" --repo "$REPO" --merge
info "Helm chart PR #$HELM_PR_NUMBER merged."
# Return to previous branch
git checkout -
echo ""
info "============================================="
info "Release $TAG complete!"
info "============================================="
info ""
info "Summary:"
info " - Release branch: $RELEASE_BRANCH"
info " - GitHub release: https://github.com/$REPO/releases/tag/$TAG"
info " - Helm chart: $CHART_VERSION (appVersion: v$APP_VERSION)"
info ""
info "The release workflow is running in the background."
info "Monitor at: https://github.com/$REPO/actions"