From f187c9143107a4179858f83c112cbe14cc59e44b Mon Sep 17 00:00:00 2001 From: ChristophShyper <45788587+ChristophShyper@users.noreply.github.com> Date: Tue, 19 May 2026 22:51:12 +0200 Subject: [PATCH] feat: improve workflow linting and update workflow files --- .dockerignore | 7 +- .../workflows/auto-pull-request-create.yml | 3 +- .github/workflows/auto-release-create.yml | 32 ++ .../manual-release-branch-prepare.yml | 39 ++ .gitignore | 9 +- Taskfile.cicd.yml | 203 +--------- Taskfile.docker.yml | 87 +--- Taskfile.scripts.yml | 373 ++++++++++++++++++ Taskfile.yml | 11 +- 9 files changed, 472 insertions(+), 292 deletions(-) create mode 100644 .github/workflows/auto-release-create.yml create mode 100644 .github/workflows/manual-release-branch-prepare.yml create mode 100644 Taskfile.scripts.yml diff --git a/.dockerignore b/.dockerignore index 6cc8f70..ad91410 100644 --- a/.dockerignore +++ b/.dockerignore @@ -3,9 +3,8 @@ # Include !Dockerfile -!entrypoint.sh -!scripts/ -!scripts/replace-template-diff.sh -!scripts/split_content_bytes.py !LICENSE !README.md +!entrypoint.sh +!scripts +!pip diff --git a/.github/workflows/auto-pull-request-create.yml b/.github/workflows/auto-pull-request-create.yml index 00baf22..e7b56cf 100644 --- a/.github/workflows/auto-pull-request-create.yml +++ b/.github/workflows/auto-pull-request-create.yml @@ -1,10 +1,11 @@ -name: (Auto) Pull Request Create +name: (Automatic) Pull Request Create on: push: branches-ignore: - master - main + - release/** - dependabot/** permissions: diff --git a/.github/workflows/auto-release-create.yml b/.github/workflows/auto-release-create.yml new file mode 100644 index 0000000..9165169 --- /dev/null +++ b/.github/workflows/auto-release-create.yml @@ -0,0 +1,32 @@ +name: (Automatic) Release Create + +on: + push: + branches: + - master + - main + workflow_dispatch: + inputs: + release_branch: + description: Release branch to publish from (e.g. release/v1.3.0) + required: false + default: '' + type: string + release_version: + description: Explicit release version override (e.g. v1.3.0) + required: false + default: '' + type: string + +permissions: + contents: write + pull-requests: read + +jobs: + call: + uses: devops-infra/.github/.github/workflows/reusable-auto-release-create.yml@v1 + with: + profile: actions + release-branch: ${{ inputs.release_branch }} + release-version: ${{ inputs.release_version }} + secrets: inherit diff --git a/.github/workflows/manual-release-branch-prepare.yml b/.github/workflows/manual-release-branch-prepare.yml new file mode 100644 index 0000000..b06ea09 --- /dev/null +++ b/.github/workflows/manual-release-branch-prepare.yml @@ -0,0 +1,39 @@ +name: (Manual) Release Branch Prepare + +on: + workflow_dispatch: + inputs: + type: + description: Bump type + required: false + default: patch + type: choice + options: + - patch + - minor + - major + - set + version: + description: Explicit version when type="set" (e.g., v1.2.3) + required: false + default: '' + build_only: + description: Build and push artifacts without version bump + required: false + default: false + type: boolean + +permissions: + contents: write + packages: write + pull-requests: write + +jobs: + call: + uses: devops-infra/.github/.github/workflows/reusable-manual-release-branch-prepare.yml@v1 + with: + bump-type: ${{ inputs.type }} + explicit-version: ${{ inputs.version }} + build-and-push-only: ${{ inputs.build_only }} + profile: actions + secrets: inherit diff --git a/.gitignore b/.gitignore index 274f060..22f7e67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,5 @@ # Intellij -/.idea/ +.idea/ *.iml # Custom @@ -10,3 +10,10 @@ .envrc .env .tmp + +# Python +build/ +dist/ +*.egg-info/ +*.pyc +__py diff --git a/Taskfile.cicd.yml b/Taskfile.cicd.yml index 45f39f1..c6baf70 100644 --- a/Taskfile.cicd.yml +++ b/Taskfile.cicd.yml @@ -1,12 +1,9 @@ version: '3' - silent: true - vars: PR_TEMPLATE: https://raw.githubusercontent.com/devops-infra/.github/refs/tags/v1/PULL_REQUEST_TEMPLATE.md CONFIGS_BASE_URL: https://raw.githubusercontent.com/devops-infra/.github/refs/tags/v1/templates/actions/configs TASKFILES_BASE_URL: https://raw.githubusercontent.com/devops-infra/.github/refs/tags/v1/templates/actions/taskfiles - tasks: pre-commit: desc: Run all pre-commit hooks @@ -29,97 +26,32 @@ tasks: lint:actionlint: desc: Lint GitHub Actions workflows with actionlint cmds: - - | - echo "▶️ Running actionlint..." - set +e - docker run --rm -i -v "$PWD:/work" -w /work rhysd/actionlint:latest -color - rc=$? - set -e - if [ "$rc" -eq 0 ]; then - echo "✅ actionlint passed" - else - echo "❌ actionlint failed" - exit $rc - fi + - task: scripts:lint:actionlint lint:hadolint: desc: Lint Dockerfile with hadolint cmds: - - | - echo "▶️ Running hadolint..." - set +e - docker run --rm -i -v "$PWD:/work" -w /work hadolint/hadolint:latest-debian < Dockerfile - rc=$? - set -e - if [ "$rc" -eq 0 ]; then - echo "✅ hadolint passed" - else - echo "❌ hadolint failed" - exit $rc - fi + - task: scripts:lint:hadolint lint:shellcheck: desc: Lint shell scripts with shellcheck cmds: - - | - echo "▶️ Running shellcheck..." - set +e - docker run --rm -i -v "$PWD:/work" -w /work koalaman/shellcheck:stable -x -S style entrypoint.sh - rc=$? - set -e - if [ "$rc" -eq 0 ]; then - echo "✅ shellcheck passed" - else - echo "❌ shellcheck failed" - exit $rc - fi + - task: scripts:lint:shellcheck lint:yamllint: desc: Lint YAML files with yamllint cmds: - - | - echo "▶️ Running yamllint..." - set +e - docker run --rm -i -v "$PWD:/work" -w /work cytopia/yamllint -c .yamllint.yml . - rc=$? - set -e - if [ "$rc" -eq 0 ]; then - echo "✅ yamllint passed" - else - echo "❌ yamllint failed" - exit $rc - fi + - task: scripts:lint:yamllint dependency:update: - desc: Check main dependency not covered by dependabot + desc: 'No-op: no dedicated dependency updater configured for this profile' cmds: - - | - echo "ℹ️ No dedicated dependency updater configured for this repository." - echo "ℹ️ Dependabot handles GitHub Actions and package metadata updates." - echo "ℹ️ Docker build validation remains the runtime safety net." + - task: scripts:dependency:update version:set: desc: Update version in README.md and action.yml cmds: - - | - if [ -z "{{.VERSION}}" ]; then - echo "❌ ERROR: VERSION is empty" - exit 1 - fi - if ! echo "{{.VERSION}}" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "❌ ERROR: VERSION '{{.VERSION}}' is not a valid semantic version (expected vX.Y.Z or X.Y.Z)" - exit 1 - fi - - echo Updating full version from {{.VERSION_FROM_ACTION_YML}} to {{.VERSION}} - - echo Updating minor version from {{.MINOR_FROM_ACTION_YML}} to {{.VERSION_MINOR}} - - echo Updating major version from {{.MAJOR_FROM_ACTION_YML}} to {{.VERSION_MAJOR}} - - "{{.SED}} -i 's#{{.DOCKER_NAME}}:{{.VERSION_FROM_ACTION_YML}}#{{.DOCKER_NAME}}:{{.VERSION}}#g' action.yml" - - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.VERSION_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION}}#g' README.md" - - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.VERSION_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION}}#g' README.md" - - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.MINOR_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION_MINOR}}#g' README.md" - - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.MINOR_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION_MINOR}}#g' README.md" - - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.MAJOR_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION_MAJOR}}#g' README.md" - - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.MAJOR_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION_MAJOR}}#g' README.md" + - task: scripts:version:set version:update:patch: desc: Increment patch version (e.g., 1.2.3 -> 1.2.4) @@ -139,135 +71,24 @@ tasks: version:resolve-next: desc: Resolve next version from bump type and profile cmds: - - | - set -eu - bump_type="${BUMP_TYPE:-patch}" - input_version="${INPUT_VERSION:-}" - - normalize_version() { - candidate="${1#v}" - if ! printf "%s" "${candidate}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then - return 1 - fi - printf "v%s" "${candidate}" - } - - current="$(task version:get 2>/dev/null || true)" - - case "$bump_type" in - set) - [ -n "$input_version" ] || { echo "Missing version for type=set"; exit 1; } - next="$(normalize_version "$input_version")" || { - echo "Invalid explicit version: $input_version. Expected vX.Y.Z or X.Y.Z" - exit 1 - } - ;; - patch|minor|major) - [ -n "$current" ] || { echo "Current version not found or invalid. Expected vX.Y.Z"; exit 1; } - current="$(normalize_version "$current")" || { echo "Current version not found or invalid. Expected vX.Y.Z"; exit 1; } - no_v="${current#v}" - major="$(printf "%s" "$no_v" | awk -F. '{print $1}')" - minor="$(printf "%s" "$no_v" | awk -F. '{print $2}')" - patch="$(printf "%s" "$no_v" | awk -F. '{print $3}')" - case "$bump_type" in - patch) next="v${major}.${minor}.$((patch + 1))" ;; - minor) next="v${major}.$((minor + 1)).0" ;; - major) next="v$((major + 1)).0.0" ;; - esac - ;; - *) - echo "Unknown type: $bump_type" - exit 1 - ;; - esac - - printf "%s" "$next" + - task: scripts:version:resolve-next version:tag-release: desc: Create set of git tags cmds: - - | - set -eu - if (set -o | grep -q pipefail) 2>/dev/null; then set -o pipefail; fi - - REMOTE='origin' - FULL='{{.VERSION_FULL}}' - MINOR='{{.VERSION_MINOR}}' - MAJOR='{{.VERSION_MAJOR}}' - - # Validate vX.Y.Z - if ! printf "%s" "$FULL" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then - echo "❌ ERROR: VERSION '$FULL' must match vX.Y.Z" >&2 - exit 1 - fi - - tag_sha() { git rev-parse "refs/tags/$1" 2>/dev/null || true; } - remote_tag_sha() { git ls-remote --tags "$REMOTE" "refs/tags/$1" 2>/dev/null | awk '{print $1}' || true; } - - echo "ℹ️ INFO: Tags - Full: $FULL | Minor: $MINOR | Major: $MAJOR" - - # Full tag: must NOT exist on remote; fail fast if it does - full_remote_sha="$(remote_tag_sha "$FULL")" - if [ -n "$full_remote_sha" ]; then - echo "❌ ERROR: Full tag '$FULL' already exists on remote; aborting" >&2 - exit 1 - fi - - # Create full tag locally (if missing) and push - if git rev-parse --quiet --verify "refs/tags/$FULL" >/dev/null 2>&1; then - echo "ℹ️ INFO: Full tag '$FULL' exists locally but not on remote; pushing" - else - echo "ℹ️ INFO: Creating full tag '$FULL'" - git tag --annotate "$FULL" --message "$FULL" - fi - git push "$REMOTE" "refs/tags/$FULL" - echo "✅ OK: Pushed full tag '$FULL'" - - # Minor tag: create or update - git tag --force --annotate "$MINOR" --message "$FULL" - minor_local_sha="$(tag_sha "$MINOR")" - minor_remote_sha="$(remote_tag_sha "$MINOR")" - if [ -z "$minor_remote_sha" ]; then - git push "$REMOTE" "refs/tags/$MINOR" - echo "✅ OK: Created and pushed minor tag '$MINOR' -> $minor_local_sha" - else - if [ "$minor_local_sha" != "$minor_remote_sha" ]; then - echo "⚠️ WARN: Updating remote minor tag '$MINOR' to $minor_local_sha (was $minor_remote_sha)" - git push --force "$REMOTE" "refs/tags/$MINOR" - else - echo "ℹ️ INFO: Minor tag '$MINOR' already up-to-date" - fi - fi - - # Major tag: create or update - git tag --force --annotate "$MAJOR" --message "$FULL" - major_local_sha="$(tag_sha "$MAJOR")" - major_remote_sha="$(remote_tag_sha "$MAJOR")" - if [ -z "$major_remote_sha" ]; then - git push "$REMOTE" "refs/tags/$MAJOR" - echo "✅ OK: Created and pushed major tag '$MAJOR' -> $major_local_sha" - else - if [ "$major_local_sha" != "$major_remote_sha" ]; then - echo "⚠️ WARN: Updating remote major tag '$MAJOR' to $major_local_sha (was $major_remote_sha)" - git push --force "$REMOTE" "refs/tags/$MAJOR" - else - echo "ℹ️ INFO: Major tag '$MAJOR' already up-to-date" - fi - fi + - task: scripts:version:tag-release git:get-pr-template: desc: Get pull request template cmds: - - mkdir -p .tmp - - curl -LsS {{.PR_TEMPLATE}} -o .tmp/PULL_REQUEST_TEMPLATE.md + - task: scripts:git:get-pr-template git:set-config: desc: Set git user config cmds: - - git config user.name "github-actions[bot]" - - git config user.email "github-actions[bot]@users.noreply.github.com" + - task: scripts:git:set-config version:get: desc: Get current version cmds: - - echo "{{.VERSION}}" + - task: scripts:version:get diff --git a/Taskfile.docker.yml b/Taskfile.docker.yml index e2e1260..f0f0baa 100644 --- a/Taskfile.docker.yml +++ b/Taskfile.docker.yml @@ -43,7 +43,7 @@ tasks: docker:cmds: desc: Show full docker build command cmds: - - echo -e '{{.DOCKER_BUILD_START}} {{.DOCKER_BUILD_FINISH}}' | {{.SED}} 's/--/ \\\n --/g' + - echo -e '{{.DOCKER_BUILD_START}} {{.DOCKER_BUILD_FINISH}}' | {{.SED}} 's/--/ \\\n+ --/g' docker:build: desc: Build Docker image @@ -51,90 +51,6 @@ tasks: - docker buildx create --use - '{{.DOCKER_BUILD_START}} {{.DOCKER_BUILD_FINISH}}' - docker:build:local: - desc: Build local runnable Docker image for current architecture - cmds: - - | - set -eu - arch="$(uname -m)" - case "${arch}" in - x86_64|amd64) target_arch="amd64" ;; - aarch64|arm64) target_arch="arm64" ;; - *) - echo "❌ Unsupported local architecture: ${arch}" - exit 1 - ;; - esac - - docker build \ - --build-arg TARGETARCH="${target_arch}" \ - --tag "{{.DOCKER_NAME}}:{{.VERSION_FULL}}{{.VERSION_SUFFIX}}" \ - --file Dockerfile \ - . - - docker:test:local: - desc: Run container-structure-test action locally (requires local build) - deps: - - task: docker:build:local - requires: - vars: [CONFIG] - cmds: - - | - set -eu - image='{{default "" .IMAGE}}' - image_from_oci_layout='{{default "" .IMAGE_FROM_OCI_LAYOUT}}' - config='{{.CONFIG}}' - - if [ -z "${image}" ] && [ -z "${image_from_oci_layout}" ]; then - echo "❌ Provide IMAGE= or IMAGE_FROM_OCI_LAYOUT=" - exit 1 - fi - if [ -n "${image}" ] && [ -n "${image_from_oci_layout}" ]; then - echo "❌ IMAGE and IMAGE_FROM_OCI_LAYOUT are mutually exclusive" - exit 1 - fi - if [ ! -S /var/run/docker.sock ] && [ '{{default "docker" .DRIVER}}' = 'docker' ]; then - echo "❌ Docker socket not available at /var/run/docker.sock (required for DRIVER=docker)" - exit 1 - fi - - docker run --rm \ - -v "$PWD:/github/workspace" \ - -w /github/workspace \ - -v /var/run/docker.sock:/var/run/docker.sock \ - -e INPUT_IMAGE="${image}" \ - -e INPUT_IMAGE_FROM_OCI_LAYOUT="${image_from_oci_layout}" \ - -e INPUT_CONFIG="${config}" \ - -e INPUT_DRIVER='{{default "docker" .DRIVER}}' \ - -e INPUT_PLATFORM='{{default "" .PLATFORM}}' \ - -e INPUT_PULL='{{default "false" .PULL}}' \ - -e INPUT_SAVE='{{default "false" .SAVE}}' \ - -e INPUT_QUIET='{{default "false" .QUIET}}' \ - -e INPUT_NO_COLOR='{{default "false" .NO_COLOR}}' \ - -e INPUT_OUTPUT='{{default "text" .OUTPUT}}' \ - -e INPUT_TEST_REPORT='{{default "" .TEST_REPORT}}' \ - -e INPUT_JUNIT_SUITE_NAME='{{default "" .JUNIT_SUITE_NAME}}' \ - -e INPUT_METADATA='{{default "" .METADATA}}' \ - -e INPUT_RUNTIME='{{default "" .RUNTIME}}' \ - -e INPUT_FORCE='{{default "false" .FORCE}}' \ - -e INPUT_DEFAULT_IMAGE_TAG='{{default "" .DEFAULT_IMAGE_TAG}}' \ - -e INPUT_IGNORE_REF_ANNOTATION='{{default "false" .IGNORE_REF_ANNOTATION}}' \ - -e INPUT_DEBUG='{{default "false" .DEBUG}}' \ - devopsinfra/action-container-structure-test:v1 - - docker:test:smoke: - desc: Smoke test local action image with container-structure-test - deps: - - task: docker:build:local - cmds: - - | - set -eu - task docker:test:local \ - IMAGE="{{.DOCKER_NAME}}:{{.VERSION_FULL}}{{.VERSION_SUFFIX}}" \ - CONFIG="tests/docker/local-image.yml" \ - DRIVER=docker \ - OUTPUT=text - docker:build:inspect: desc: Inspect built Docker image cmds: @@ -164,7 +80,6 @@ tasks: rc=$? set -e - # Validate that docker inspect returned a non-empty array with an Id has_local=0 if [ "$rc" -eq 0 ] && [ -n "$image_inspect_out" ]; then if echo "$image_inspect_out" | jq -e 'type=="array" and (length > 0) and \ diff --git a/Taskfile.scripts.yml b/Taskfile.scripts.yml new file mode 100644 index 0000000..429d778 --- /dev/null +++ b/Taskfile.scripts.yml @@ -0,0 +1,373 @@ +version: '3' + +silent: true + +tasks: + help: + desc: Detailed help + cmds: + - | + echo "Tasks:" + task --list + + lint:actionlint: + desc: Lint GitHub Actions workflows with actionlint + cmds: + - | + echo "▶️ Running actionlint..." + set +e + docker run --rm -i -v "$PWD:/work" -w /work rhysd/actionlint:latest -color + rc=$? + set -e + if [ "$rc" -eq 0 ]; then + echo "✅ actionlint passed" + else + echo "❌ actionlint failed" + exit $rc + fi + + lint:hadolint: + desc: Lint Dockerfile with hadolint + cmds: + - | + echo "▶️ Running hadolint..." + set +e + docker run --rm -i -v "$PWD:/work" -w /work hadolint/hadolint:latest-debian < Dockerfile + rc=$? + set -e + if [ "$rc" -eq 0 ]; then + echo "✅ hadolint passed" + else + echo "❌ hadolint failed" + exit $rc + fi + + lint:shellcheck: + desc: Lint shell scripts with shellcheck + cmds: + - | + echo "▶️ Running shellcheck..." + set +e + docker run --rm -i -v "$PWD:/work" -w /work koalaman/shellcheck:stable -x -S style entrypoint.sh + rc=$? + set -e + if [ "$rc" -eq 0 ]; then + echo "✅ shellcheck passed" + else + echo "❌ shellcheck failed" + exit $rc + fi + + lint:yamllint: + desc: Lint YAML files with yamllint + cmds: + - | + echo "▶️ Running yamllint..." + set +e + docker run --rm -i -v "$PWD:/work" -w /work cytopia/yamllint -c .yamllint.yml . + rc=$? + set -e + if [ "$rc" -eq 0 ]; then + echo "✅ yamllint passed" + else + echo "❌ yamllint failed" + exit $rc + fi + + dependency:update: + desc: 'No-op: no dedicated dependency updater configured for this profile' + cmds: + - | + echo "INFO: No dedicated dependency updater configured for this repository profile." + echo "INFO: Dependabot handles GitHub Actions and package metadata updates." + echo "INFO: Keep this task as a safe no-op until a repo-specific dependency updater is defined." + + version:get: + desc: Get current version + cmds: + - echo "{{.VERSION}}" + + version:set: + desc: Update version in README.md and action.yml + cmds: + - | + if [ -z "{{.VERSION}}" ]; then + echo "❌ ERROR: VERSION is empty" + exit 1 + fi + if ! echo "{{.VERSION}}" | grep -Eq '^v?[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "❌ ERROR: VERSION '{{.VERSION}}' is not a valid semantic version (expected vX.Y.Z or X.Y.Z)" + exit 1 + fi + - echo Updating full version from {{.VERSION_FROM_ACTION_YML}} to {{.VERSION}} + - echo Updating minor version from {{.MINOR_FROM_ACTION_YML}} to {{.VERSION_MINOR}} + - echo Updating major version from {{.MAJOR_FROM_ACTION_YML}} to {{.VERSION_MAJOR}} + - "{{.SED}} -i 's#{{.DOCKER_NAME}}:{{.VERSION_FROM_ACTION_YML}}#{{.DOCKER_NAME}}:{{.VERSION}}#g' action.yml" + - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.VERSION_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION}}#g' README.md" + - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.VERSION_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION}}#g' README.md" + - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.MINOR_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION_MINOR}}#g' README.md" + - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.MINOR_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION_MINOR}}#g' README.md" + - "{{.SED}} -i 's#{{.DOCKER_NAME}}@{{.MAJOR_FROM_ACTION_YML}}#{{.DOCKER_NAME}}@{{.VERSION_MAJOR}}#g' README.md" + - "{{.SED}} -i 's#{{.GITHUB_NAME}}@{{.MAJOR_FROM_ACTION_YML}}#{{.GITHUB_NAME}}@{{.VERSION_MAJOR}}#g' README.md" + + version:resolve-next: + desc: Resolve next version from bump type and profile + cmds: + - | + set -eu + bump_type="${BUMP_TYPE:-patch}" + input_version="${INPUT_VERSION:-}" + + normalize_version() { + candidate="${1#v}" + if ! printf "%s" "${candidate}" | grep -Eq '^[0-9]+\.[0-9]+\.[0-9]+$'; then + return 1 + fi + printf "v%s" "${candidate}" + } + + current="$(task version:get 2>/dev/null || true)" + + case "$bump_type" in + set) + [ -n "$input_version" ] || { echo "Missing version for type=set"; exit 1; } + next="$(normalize_version "$input_version")" || { + echo "Invalid explicit version: $input_version. Expected vX.Y.Z or X.Y.Z" + exit 1 + } + ;; + patch|minor|major) + [ -n "$current" ] || { echo "Current version not found or invalid. Expected vX.Y.Z"; exit 1; } + current="$(normalize_version "$current")" || { echo "Current version not found or invalid. Expected vX.Y.Z"; exit 1; } + no_v="${current#v}" + major="$(printf "%s" "$no_v" | awk -F. '{print $1}')" + minor="$(printf "%s" "$no_v" | awk -F. '{print $2}')" + patch="$(printf "%s" "$no_v" | awk -F. '{print $3}')" + case "$bump_type" in + patch) next="v${major}.${minor}.$((patch + 1))" ;; + minor) next="v${major}.$((minor + 1)).0" ;; + major) next="v$((major + 1)).0.0" ;; + esac + ;; + *) + echo "Unknown type: $bump_type" + exit 1 + ;; + esac + + printf "%s" "$next" + + version:tag-release: + desc: Create set of git tags + cmds: + - | + set -eu + if (set -o | grep -q pipefail) 2>/dev/null; then set -o pipefail; fi + + REMOTE='origin' + FULL='{{.VERSION_FULL}}' + MINOR='{{.VERSION_MINOR}}' + MAJOR='{{.VERSION_MAJOR}}' + + # Validate vX.Y.Z + if ! printf "%s" "$FULL" | grep -Eq '^v[0-9]+\.[0-9]+\.[0-9]+$'; then + echo "❌ ERROR: VERSION '$FULL' must match vX.Y.Z" >&2 + exit 1 + fi + + tag_sha() { git rev-parse "refs/tags/$1" 2>/dev/null || true; } + remote_tag_sha() { git ls-remote --tags "$REMOTE" "refs/tags/$1" 2>/dev/null | awk '{print $1}' || true; } + + echo "ℹ️ INFO: Tags - Full: $FULL | Minor: $MINOR | Major: $MAJOR" + + # Full tag: must NOT exist on remote; fail fast if it does + full_remote_sha="$(remote_tag_sha "$FULL")" + if [ -n "$full_remote_sha" ]; then + echo "❌ ERROR: Full tag '$FULL' already exists on remote; aborting" >&2 + exit 1 + fi + + # Create full tag locally (if missing) and push + if git rev-parse --quiet --verify "refs/tags/$FULL" >/dev/null 2>&1; then + echo "ℹ️ INFO: Full tag '$FULL' exists locally but not on remote; pushing" + else + echo "ℹ️ INFO: Creating full tag '$FULL'" + git tag --annotate "$FULL" --message "$FULL" + fi + git push "$REMOTE" "refs/tags/$FULL" + echo "✅ OK: Pushed full tag '$FULL'" + + # Minor tag: create or update + git tag --force --annotate "$MINOR" --message "$FULL" + minor_local_sha="$(tag_sha "$MINOR")" + minor_remote_sha="$(remote_tag_sha "$MINOR")" + if [ -z "$minor_remote_sha" ]; then + git push "$REMOTE" "refs/tags/$MINOR" + echo "✅ OK: Created and pushed minor tag '$MINOR' -> $minor_local_sha" + else + if [ "$minor_local_sha" != "$minor_remote_sha" ]; then + echo "⚠️ WARN: Updating remote minor tag '$MINOR' to $minor_local_sha (was $minor_remote_sha)" + git push --force "$REMOTE" "refs/tags/$MINOR" + else + echo "ℹ️ INFO: Minor tag '$MINOR' already up-to-date" + fi + fi + + # Major tag: create or update + git tag --force --annotate "$MAJOR" --message "$FULL" + major_local_sha="$(tag_sha "$MAJOR")" + major_remote_sha="$(remote_tag_sha "$MAJOR")" + if [ -z "$major_remote_sha" ]; then + git push "$REMOTE" "refs/tags/$MAJOR" + echo "✅ OK: Created and pushed major tag '$MAJOR' -> $major_local_sha" + else + if [ "$major_local_sha" != "$major_remote_sha" ]; then + echo "⚠️ WARN: Updating remote major tag '$MAJOR' to $major_local_sha (was $major_remote_sha)" + git push --force "$REMOTE" "refs/tags/$MAJOR" + else + echo "ℹ️ INFO: Major tag '$MAJOR' already up-to-date" + fi + fi + + git:get-pr-template: + desc: Get pull request template + cmds: + - mkdir -p .tmp + - curl -LsS https://raw.githubusercontent.com/devops-infra/.github/refs/tags/v1/PULL_REQUEST_TEMPLATE.md -o .tmp/PULL_REQUEST_TEMPLATE.md + + git:set-config: + desc: Set git user config + cmds: + - git config user.name "github-actions[bot]" + - git config user.email "github-actions[bot]@users.noreply.github.com" + + packages:update: + desc: Update Alpine package pins in alpine-packages.txt + cmds: + - | + set -eu + if [ ! -f Dockerfile ]; then + echo "INFO: Dockerfile not found; nothing to update" + exit 0 + fi + if [ ! -f alpine-packages.txt ]; then + echo "INFO: alpine-packages.txt not found; nothing to update" + exit 0 + fi + + base_image="$(sed -nE 's/^FROM[[:space:]]+([^[:space:]]+).*/\1/p' Dockerfile | head -1)" + if [ -z "$base_image" ]; then + echo "INFO: Could not resolve base image; nothing to update" + exit 0 + fi + + case "$base_image" in + alpine:*|alpine) + : + ;; + *) + echo "INFO: Base image is '$base_image', not Alpine; nothing to update" + exit 0 + ;; + esac + + alpine_line="${base_image#alpine:}" + if [ "$alpine_line" = "$base_image" ] || [ -z "$alpine_line" ]; then + echo "INFO: Could not parse Alpine version from '$base_image'; nothing to update" + exit 0 + fi + alpine_minor="$(printf '%s' "$alpine_line" | awk -F. '{print $1 "." $2}')" + if ! printf '%s' "$alpine_minor" | grep -Eq '^[0-9]+\.[0-9]+$'; then + echo "INFO: Unsupported Alpine version '$alpine_line'; nothing to update" + exit 0 + fi + alpine_repo="v${alpine_minor}" + arch="x86_64" + + normalize_minor() { + version="$1" + printf '%s' "$version" | sed -E 's/^([0-9]+\.[0-9]+).*/\1/' + } + + fetch_index() { + repo="$1" + out="$2" + url="https://dl-cdn.alpinelinux.org/alpine/${alpine_repo}/${repo}/${arch}/APKINDEX.tar.gz" + curl --fail --silent --show-error "$url" | tar -O -zx APKINDEX > "$out" + } + + lookup_latest() { + pkg="$1" + for index in "$index_main" "$index_community"; do + found="$(awk -v pkg="$pkg" ' + BEGIN { RS=""; FS="\n" } + { + p=""; v="" + for (i=1; i<=NF; i++) { + if ($i ~ /^P:/) p=substr($i,3) + if ($i ~ /^V:/) v=substr($i,3) + } + if (p==pkg) { print v; exit } + } + ' "$index")" + if [ -n "$found" ]; then + printf '%s' "$found" + return 0 + fi + done + return 1 + } + + mkdir -p .tmp + index_main=".tmp/apkindex-main-${alpine_repo}-${arch}.txt" + index_community=".tmp/apkindex-community-${alpine_repo}-${arch}.txt" + fetch_index main "$index_main" + fetch_index community "$index_community" + + if ! grep -Eq '^[a-zA-Z0-9+_.-]+(=~|~=)[0-9]+\.[0-9]+$' alpine-packages.txt; then + echo "INFO: No pinned Alpine packages (~=X.Y) found in alpine-packages.txt" + exit 0 + fi + + tmp_out=".tmp/alpine-packages.updated.txt" + : > "$tmp_out" + updated=0 + while IFS= read -r line || [ -n "$line" ]; do + if [ -z "$line" ] || printf '%s' "$line" | grep -Eq '^[[:space:]]*#'; then + echo "$line" >> "$tmp_out" + continue + fi + if ! printf '%s' "$line" | grep -Eq '^[a-zA-Z0-9+_.-]+(=~|~=)[0-9]+\.[0-9]+$'; then + echo "$line" >> "$tmp_out" + continue + fi + + pkg="$(printf '%s' "$line" | sed -E 's/^([a-zA-Z0-9+_.-]+)(=~|~=).*/\1/')" + current_minor="$(printf '%s' "$line" | sed -E 's/^[a-zA-Z0-9+_.-]+(=~|~=)([0-9]+\.[0-9]+).*$/\2/')" + latest_full="$(lookup_latest "$pkg" || true)" + if [ -z "$latest_full" ]; then + echo "WARN: Could not resolve latest version for $pkg; keeping $line" + echo "$line" >> "$tmp_out" + continue + fi + + latest_minor="$(normalize_minor "$latest_full")" + if [ "$latest_minor" = "$current_minor" ]; then + echo "OK: $pkg already up to date at $current_minor" + echo "$pkg~=$current_minor" >> "$tmp_out" + continue + fi + echo "UPDATE: $pkg $current_minor -> $latest_minor" + echo "$pkg~=$latest_minor" >> "$tmp_out" + updated=1 + done < alpine-packages.txt + + if ! cmp -s alpine-packages.txt "$tmp_out"; then + mv "$tmp_out" alpine-packages.txt + else + rm -f "$tmp_out" + fi + + if [ "$updated" -eq 0 ]; then + echo "INFO: No Alpine package updates were required" + fi diff --git a/Taskfile.yml b/Taskfile.yml index 89d55c8..a065cbc 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -7,6 +7,7 @@ dotenv: includes: variables: ./Taskfile.variables.yml + scripts: ./Taskfile.scripts.yml cicd: taskfile: ./Taskfile.cicd.yml flatten: true @@ -23,12 +24,4 @@ tasks: help: desc: Detailed help cmds: - - | - echo "Tasks:" - task --list - echo "" - echo "Environment:" - echo " DOCKER_NAME={{.DOCKER_NAME}} DOCKER_USERNAME={{.DOCKER_USERNAME}}" - echo " GHRC_NAME={{.GHRC_NAME}} GITHUB_USERNAME={{.GITHUB_USERNAME}}" - echo " LAST_RELEASE={{.LAST_RELEASE}}" VERSION={{.VERSION}} VERSION_FULL={{.VERSION_FULL}} - echo " BRANCH={{.GIT_BRANCH}} GIT_SHORT_SHA={{.GIT_SHORT_SHA}}" GIT_SHA={{.GIT_SHA}} + - task: scripts:help