diff --git a/.dockerignore b/.dockerignore index a700b20..1bc823c 100644 --- a/.dockerignore +++ b/.dockerignore @@ -4,6 +4,7 @@ # Include !alpine-packages.txt !Dockerfile -!entrypoint.sh !LICENSE !README.md +!entrypoint.sh +!pip diff --git a/.editorconfig b/.editorconfig index 317b9eb..99580d0 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,4 +1,3 @@ -# EditorConfig helps developers define and maintain consistent coding styles root = true [*] 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 b75cdc1..22f7e67 100644 --- a/.gitignore +++ b/.gitignore @@ -1,11 +1,19 @@ # Intellij -/.idea/ +.idea/ *.iml # Custom +.DS_Store .tmp/ .venv .venv/ .envrc .env .tmp + +# Python +build/ +dist/ +*.egg-info/ +*.pyc +__py diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 32ffeba..7e16b3a 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,16 +31,16 @@ repos: pass_filenames: false - id: hadolint name: hadolint - entry: bash -lc 'docker run --rm -v "$PWD:/work" -w /work hadolint/hadolint:latest-debian hadolint Dockerfile' + entry: bash -lc 'docker run --rm -v "$PWD:/work" -w /work hadolint/hadolint:latest-debian /bin/hadolint "$@"' -- language: system - pass_filenames: false + files: (^|/)Dockerfile(\..*)?$ - id: shellcheck name: shellcheck - entry: bash -lc 'docker run --rm -v "$PWD:/work" -w /work koalaman/shellcheck:stable -x -S style entrypoint.sh' + entry: bash -lc 'docker run --rm -v "$PWD:/work" -w /work koalaman/shellcheck:stable -x -S style "$@"' -- language: system - pass_filenames: false + files: \.sh$ - id: yamllint name: yamllint - entry: bash -lc 'docker run --rm -v "$PWD:/work" -w /work cytopia/yamllint -c .yamllint.yml .' + entry: bash -lc 'docker run --rm -v "$PWD:/work" -w /work cytopia/yamllint -c .yamllint.yml "$@"' -- language: system - pass_filenames: false + files: \.(yml|yaml)$ diff --git a/.yamllint.yml b/.yamllint.yml index d2eec4f..fb0e11a 100644 --- a/.yamllint.yml +++ b/.yamllint.yml @@ -2,6 +2,7 @@ extends: default rules: empty-lines: max: 2 + max-end: 1 document-end: present: false document-start: @@ -11,14 +12,12 @@ rules: indent-sequences: true check-multi-line-strings: false line-length: - max: 140 + max: 220 allow-non-breakable-inline-mappings: true new-line-at-end-of-file: enable new-lines: type: unix - quoted-strings: - required: only-when-needed - extra-allowed: ['true', 'false'] + quoted-strings: disable trailing-spaces: {} truthy: allowed-values: ['true', 'false', 'yes', 'no'] diff --git a/Taskfile.cicd.yml b/Taskfile.cicd.yml index 7395db8..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 @@ -26,138 +23,35 @@ tasks: - task: lint:shellcheck - task: lint:yamllint - test: - desc: Run repository test suite - cmds: - - task: test:entrypoint - - test:entrypoint: - desc: Run entrypoint unit tests - cmds: - - | - echo "▶️ Running entrypoint tests..." - set +e - bash tests/test_entrypoint.sh - rc=$? - set -e - if [ "$rc" -eq 0 ]; then - echo "✅ entrypoint tests passed" - else - echo "❌ entrypoint tests failed" - exit $rc - fi - 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: Update repository dependencies not covered by dependabot + desc: 'No-op: no dedicated dependency updater configured for this profile' cmds: - - task: scripts:packages:update - - | - set -eu - latest="$(curl -LsS https://api.github.com/repos/GoogleContainerTools/container-structure-test/releases/latest | jq -r '.tag_name')" - if [ -z "${latest}" ] || [ "${latest}" = "null" ]; then - echo "❌ Could not resolve latest container-structure-test version" - exit 1 - fi - current="$(sed -nE 's/^ARG CST_VERSION=(v[0-9.]+)$/\1/p' Dockerfile | head -1)" - if [ -z "${current}" ]; then - echo "❌ Could not resolve current CST_VERSION from Dockerfile" - exit 1 - fi - echo "Current CST_VERSION: ${current}" - echo "Latest CST_VERSION: ${latest}" - if [ "${current}" = "${latest}" ]; then - echo "✅ CST_VERSION already up to date" - exit 0 - fi - {{.SED}} -i "s#^ARG CST_VERSION=${current}$#ARG CST_VERSION=${latest}#" Dockerfile - echo "✅ Updated Dockerfile CST_VERSION to ${latest}" + - 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) @@ -177,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 ebaee09..f0f0baa 100644 --- a/Taskfile.docker.yml +++ b/Taskfile.docker.yml @@ -5,109 +5,51 @@ silent: true tasks: docker:login: desc: Login to hub.docker.com and ghcr.io - cmds: - - echo "Logging into Docker Hub as {{.DOCKER_USERNAME}}" - - echo "${DOCKER_TOKEN}" | docker login -u "{{.DOCKER_USERNAME}}" --password-stdin - - echo "Logging into GHCR as {{.GITHUB_USERNAME}}" - - echo "${GITHUB_TOKEN}" | docker login ghcr.io -u "{{.GITHUB_USERNAME}}" --password-stdin - - docker:cmds: - desc: Show full docker build command - cmds: - - echo -e '{{.DOCKER_BUILD_START}} {{.DOCKER_BUILD_FINISH}}' | {{.SED}} 's/--/ \\\n --/g' - - docker:build: - desc: Build Docker image - cmds: - - 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 + docker_username='{{.DOCKER_USERNAME}}' + github_username='{{.GITHUB_USERNAME}}' + has_dockerhub=false + has_ghcr=false - image='{{default "" .IMAGE}}' - image_from_oci_layout='{{default "" .IMAGE_FROM_OCI_LAYOUT}}' - config='{{.CONFIG}}' + if [ -n "$docker_username" ] && [ -n "${DOCKER_TOKEN:-}" ]; then + has_dockerhub=true + fi - if [ -z "${image}" ] && [ -z "${image_from_oci_layout}" ]; then - echo "❌ Provide IMAGE= or IMAGE_FROM_OCI_LAYOUT=" - exit 1 + if [ -n "$github_username" ] && [ -n "${GITHUB_TOKEN:-}" ]; then + has_ghcr=true fi - if [ -n "${image}" ] && [ -n "${image_from_oci_layout}" ]; then - echo "❌ IMAGE and IMAGE_FROM_OCI_LAYOUT are mutually exclusive" + if [ "$has_dockerhub" = false ] && [ "$has_ghcr" = false ]; then + echo "❌ No registry credentials provided. Set DOCKER_USERNAME/DOCKER_TOKEN or GITHUB_USERNAME/GITHUB_TOKEN." 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 + if [ "$has_dockerhub" = true ]; then + echo "Logging into Docker Hub as $docker_username" + printf '%s' "${DOCKER_TOKEN}" | docker login -u "$docker_username" --password-stdin + else + echo "⚠️ Skipping Docker Hub login (missing DOCKER_USERNAME/DOCKER_TOKEN)" 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}}' \ - "{{.DOCKER_NAME}}:{{.VERSION_FULL}}{{.VERSION_SUFFIX}}" - - docker:test:smoke: - desc: Smoke test local action image with container-structure-test - deps: - - task: docker:build:local + if [ "$has_ghcr" = true ]; then + echo "Logging into GHCR as $github_username" + printf '%s' "${GITHUB_TOKEN}" | docker login ghcr.io -u "$github_username" --password-stdin + else + echo "⚠️ Skipping GHCR login (missing GITHUB_USERNAME/GITHUB_TOKEN)" + fi + + docker:cmds: + desc: Show full docker build command cmds: - - | - set -eu - task docker:test:local \ - IMAGE="{{.DOCKER_NAME}}:{{.VERSION_FULL}}{{.VERSION_SUFFIX}}" \ - CONFIG="tests/docker/local-image.yml" \ - DRIVER=docker \ - OUTPUT=text + - echo -e '{{.DOCKER_BUILD_START}} {{.DOCKER_BUILD_FINISH}}' | {{.SED}} 's/--/ \\\n+ --/g' + + docker:build: + desc: Build Docker image + cmds: + - docker buildx create --use + - '{{.DOCKER_BUILD_START}} {{.DOCKER_BUILD_FINISH}}' docker:build:inspect: desc: Inspect built Docker image @@ -138,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 index c9fca95..429d778 100644 --- a/Taskfile.scripts.yml +++ b/Taskfile.scripts.yml @@ -9,12 +9,237 @@ tasks: - | 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}} + + 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 diff --git a/Taskfile.yml b/Taskfile.yml index 3719b8b..a065cbc 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -2,6 +2,9 @@ version: '3' silent: true +dotenv: + - .env + includes: variables: ./Taskfile.variables.yml scripts: ./Taskfile.scripts.yml diff --git a/alpine-packages.txt b/alpine-packages.txt index b0b27ab..812f3eb 100644 --- a/alpine-packages.txt +++ b/alpine-packages.txt @@ -1,3 +1,3 @@ bash~=5.3 -curl~=8.19 -jq~=1.8 +curl +jq