diff --git a/.github/actions/determine-semver/action.yml b/.github/actions/determine-semver/action.yml index 883bd95..2b75271 100644 --- a/.github/actions/determine-semver/action.yml +++ b/.github/actions/determine-semver/action.yml @@ -3,12 +3,9 @@ description: 'Reusable action to compute semantic version from major/minor and l runs: using: "composite" steps: - - name: Announce version computation + - name: Initialize version computation shell: bash - run: | - echo "::notice title=🏷️ [VERSION] Determine Semantic Version::Computing next version β€” major: ${{ inputs.major }}, minor: ${{ inputs.minor }}, actor: ${{ github.actor }}" - echo "πŸ“‹ [VERSION] Scanning git tags for pattern: ${{ inputs.major }}.${{ inputs.minor }}.*" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" + run: echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - name: Determine semantic version id: semver @@ -69,10 +66,9 @@ runs: fi version="$major.$minor.$patch" - echo "Computed next version: $version" echo "version=$version" >> $GITHUB_OUTPUT echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [VERSION] Version computed::Next version: ${version}" + echo "βœ… Computed version: ${version}" echo "::endgroup::" shell: bash diff --git a/.github/actions/docker-build-push/action.yml b/.github/actions/docker-build-push/action.yml index 1b23d5e..a3f2673 100644 --- a/.github/actions/docker-build-push/action.yml +++ b/.github/actions/docker-build-push/action.yml @@ -81,12 +81,23 @@ outputs: runs: using: 'composite' steps: - - name: Announce Docker build + - name: Initialize Docker build shell: bash + env: + IMAGE_NAME: ${{ inputs.image-name }} + VERSION: ${{ inputs.version }} + REGISTRY: ${{ inputs.registry }} + PLATFORMS: ${{ inputs.platforms }} run: | - echo "::notice title=🐳 Docker Build::${{ inputs.image-name }}:${{ inputs.version }} β†’ ${{ inputs.registry }} (${{ inputs.platforms }})" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" + echo "::group::🐳 Docker build context" + printf '%-12s%s\n' 'Image' "${IMAGE_NAME}:${VERSION}" + printf '%-12s%s\n' 'Registry' "${REGISTRY}" + printf '%-12s%s\n' 'Platforms' "${PLATFORMS}" + echo "::endgroup::" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Set up QEMU # Only needed for non-native/multi-arch builds; skip for the single-arch linux/amd64 default to save setup time. @@ -105,9 +116,11 @@ runs: - name: Confirm registry auth shell: bash + env: + REGISTRY: ${{ inputs.registry }} run: | echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "βœ… [CHECKPOINT 1/2] Registry auth β€” PASSED (${{ inputs.registry }})" + echo "βœ… [1/2] Registry auth β€” ${REGISTRY}" - name: Extract metadata id: meta @@ -145,12 +158,20 @@ runs: - name: Confirm image built and pushed shell: bash + env: + IMAGE_NAME: ${{ inputs.image-name }} + VERSION: ${{ inputs.version }} + DIGEST: ${{ steps.build.outputs.digest }} run: | echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… Docker image pushed::${{ inputs.image-name }}:${{ inputs.version }} β€” digest: ${{ steps.build.outputs.digest }}" + echo "βœ… [2/2] Image pushed β€” ${IMAGE_NAME}:${VERSION} (${DIGEST})" - name: Report failure if: failure() shell: bash + env: + IMAGE_NAME: ${{ inputs.image-name }} + VERSION: ${{ inputs.version }} + REGISTRY: ${{ inputs.registry }} run: | - echo "::error title=❌ Docker build failed::${{ inputs.image-name }}:${{ inputs.version }} on ${{ inputs.registry }} β€” CP1: ${CHECKPOINT_1_STATUS:-⏭️ Not reached} | CP2: ${CHECKPOINT_2_STATUS:-⏭️ Not reached}" + echo "::error title=Docker build failed for ${IMAGE_NAME}:${VERSION}::Registry ${REGISTRY} β€” auth: ${CHECKPOINT_1_STATUS:-❌ Not reached} | build/push: ${CHECKPOINT_2_STATUS:-❌ Not reached}" diff --git a/.github/actions/dotnet-build/action.yml b/.github/actions/dotnet-build/action.yml index d60c3a5..ab8fb10 100644 --- a/.github/actions/dotnet-build/action.yml +++ b/.github/actions/dotnet-build/action.yml @@ -3,18 +3,28 @@ description: 'Reusable action to restore, build, and optionally test .NET projec runs: using: "composite" steps: - - name: Announce .NET build + - name: Initialize .NET build shell: bash run: | - echo "::notice title=πŸ”· [DOTNET] Restore and Build::projects: ${{ inputs.projects }}, config: ${{ inputs.configuration }}, dotnet: ${{ inputs.dotnet-version }}, tests: ${{ inputs.run-tests }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_3_STATUS=⏭️ Skipped" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + echo "CHECKPOINT_3_STATUS=⏭️ Skipped" + } >> "$GITHUB_ENV" - name: Setup .NET uses: actions/setup-dotnet@v5 with: dotnet-version: ${{ inputs.dotnet-version }} + - name: Cache NuGet packages + uses: actions/cache@v4 + with: + path: ~/.nuget/packages + # Keyed on project/package manifests so the cache invalidates when + # dependencies change; restore-keys give a warm partial hit otherwise. + key: ${{ runner.os }}-nuget-${{ hashFiles('**/*.csproj', '**/packages.lock.json', '**/Directory.Packages.props') }} + restore-keys: | + ${{ runner.os }}-nuget- - name: Restore dependencies run: | echo "::group::πŸ”· [CHECKPOINT 1/3] Restore Dependencies" diff --git a/.github/actions/dotnet-pack-push/action.yml b/.github/actions/dotnet-pack-push/action.yml index 8a9fa2e..c4b6e10 100644 --- a/.github/actions/dotnet-pack-push/action.yml +++ b/.github/actions/dotnet-pack-push/action.yml @@ -3,12 +3,13 @@ description: 'Reusable action to pack .NET project(s) and push the resulting NuG runs: using: "composite" steps: - - name: Announce NuGet pack and push + - name: Initialize NuGet pack and push shell: bash run: | - echo "::notice title=πŸ”· [DOTNET] Pack and Push NuGet::projects: ${{ inputs.projects }}, version: ${{ inputs.version != '' && inputs.version || 'from project' }}, feed: ${{ inputs.nuget-source }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Pack NuGet packages run: | @@ -49,7 +50,7 @@ runs: for project in $project_files; do # Skip if project file doesn't exist if [ ! -f "$project" ]; then - echo "Warning: Project file '$project' not found, skipping..." + echo "::warning title=NuGet project not found::Skipping '$project' β€” file does not exist." continue fi @@ -84,7 +85,7 @@ runs: exit 1 fi echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [DOTNET] NuGet packages packed::$packed_count package(s) ready" + echo "βœ… [1/2] Packed $packed_count project(s)" echo "::endgroup::" shell: bash - name: Push NuGet packages @@ -144,9 +145,8 @@ runs: exit 1 fi - echo "All packages pushed successfully!" echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [DOTNET] NuGet packages pushed::$pushed_count package(s) β†’ ${{ inputs.nuget-source }}" + echo "βœ… [2/2] Pushed $pushed_count package(s) β†’ ${{ inputs.nuget-source }}" echo "::endgroup::" shell: bash diff --git a/.github/actions/generate-wrangler-config/action.yml b/.github/actions/generate-wrangler-config/action.yml index 7a334bb..c24d7d5 100644 --- a/.github/actions/generate-wrangler-config/action.yml +++ b/.github/actions/generate-wrangler-config/action.yml @@ -37,12 +37,12 @@ inputs: runs: using: "composite" steps: - - name: Announce wrangler.toml generation + - name: Initialize wrangler.toml generation shell: bash run: | - echo "::notice title=⚑ [CF-WORKERS] Generate wrangler.toml::Generating config β€” project: ${{ inputs.PROJECT_NAME }}, env: ${{ inputs.ENVIRONMENT }}, opennext: ${{ inputs.BUILD_FOR_OPENNEXT }}" - echo "πŸ“‹ [CF-WORKERS] Routes: ${{ inputs.ROUTE }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Generate wrangler.toml shell: bash @@ -123,7 +123,7 @@ runs: echo "---------------------------" cat wrangler.toml echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [CF-WORKERS] wrangler.toml generated::Project: ${PROJECT_NAME}, env: ${ENVIRONMENT}, routes: ${#ROUTES[@]}" + echo "βœ… wrangler.toml generated β€” project: ${PROJECT_NAME}, env: ${ENVIRONMENT}, routes: ${#ROUTES[@]}" echo "::endgroup::" - name: Report failure diff --git a/.github/actions/helm-deploy-s9generic/action.yml b/.github/actions/helm-deploy-s9generic/action.yml index 32b5a91..fbb4cbe 100644 --- a/.github/actions/helm-deploy-s9generic/action.yml +++ b/.github/actions/helm-deploy-s9generic/action.yml @@ -96,13 +96,13 @@ outputs: runs: using: 'composite' steps: - - name: Announce Helm deployment + - name: Initialize Helm deployment shell: bash run: | - echo "::notice title=☸️ [HELM] Deploy Chart (s9generic)::Release: ${{ inputs.release-name }}, chart: ${{ inputs.chart-name }}:${{ inputs.chart-version }}, namespace: ${{ inputs.namespace }}" - echo "🌍 [HELM] Chart source: ${{ inputs.chart-path != '' && 'local-artifact' || 'oci-registry' }}, wait: ${{ inputs.wait }}, timeout: ${{ inputs.timeout }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Validate inputs shell: bash @@ -151,7 +151,7 @@ runs: echo "Cluster info:" kubectl cluster-info echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Kubernetes context configured::namespace: ${{ inputs.namespace }}" + echo "βœ… Kubernetes context configured β€” namespace: ${{ inputs.namespace }}" echo "::endgroup::" - name: Create namespace @@ -375,7 +375,7 @@ runs: echo "status=deployed" >> $GITHUB_OUTPUT echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Chart deployed::Release: ${{ inputs.release-name }}, namespace: ${{ inputs.namespace }}" + echo "βœ… Chart deployed β€” release: ${{ inputs.release-name }}, namespace: ${{ inputs.namespace }}" - name: Verify deployment shell: bash diff --git a/.github/actions/helm-deploy/action.yml b/.github/actions/helm-deploy/action.yml index 7919b61..10bdb2f 100644 --- a/.github/actions/helm-deploy/action.yml +++ b/.github/actions/helm-deploy/action.yml @@ -99,13 +99,13 @@ outputs: runs: using: 'composite' steps: - - name: Announce Helm deployment + - name: Initialize Helm deployment shell: bash run: | - echo "::notice title=☸️ [HELM] Deploy Chart::Release: ${{ inputs.release-name }}, chart: ${{ inputs.chart-name }}:${{ inputs.chart-version }}, namespace: ${{ inputs.namespace }}, source: ${{ inputs.chart-source-type }}" - echo "🌍 [HELM] Target namespace: ${{ inputs.namespace }}, wait: ${{ inputs.wait }}, timeout: ${{ inputs.timeout }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Set up Helm uses: azure/setup-helm@v5 @@ -132,7 +132,7 @@ runs: echo "Cluster info:" kubectl cluster-info echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Kubernetes context configured::namespace: ${{ inputs.namespace }}" + echo "βœ… Kubernetes context configured β€” namespace: ${{ inputs.namespace }}" echo "::endgroup::" - name: Create namespace @@ -307,7 +307,7 @@ runs: echo "status=deployed" >> $GITHUB_OUTPUT echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Chart deployed::Release: ${{ inputs.release-name }}, namespace: ${{ inputs.namespace }}" + echo "βœ… Chart deployed β€” release: ${{ inputs.release-name }}, namespace: ${{ inputs.namespace }}" - name: Verify deployment shell: bash diff --git a/.github/actions/helm-generic/action.yml b/.github/actions/helm-generic/action.yml index 31a70a4..01928b1 100644 --- a/.github/actions/helm-generic/action.yml +++ b/.github/actions/helm-generic/action.yml @@ -90,20 +90,13 @@ inputs: runs: using: 'composite' steps: - - name: Announce Helm deployment + - name: Initialize Helm deployment shell: bash - env: - APP_NAME: ${{ inputs.app_name }} - CHART: ${{ inputs.chart }} - NAMESPACE: ${{ inputs.namespace }} - ENVIRONMENT: ${{ inputs.environment }} - CHART_REPO: ${{ inputs.chart_repo }} - HELM_TIMEOUT: ${{ inputs.helm_timeout }} run: | - echo "::notice title=☸️ [HELM] Deploy Chart::App: ${APP_NAME}, chart: ${CHART}, namespace: ${NAMESPACE}, env: ${ENVIRONMENT}" - echo "βš™οΈ [HELM] Chart repo: ${CHART_REPO:-OCI (inline)}, timeout: ${HELM_TIMEOUT}" - echo "HELM_GENERIC_KUBECONFIG_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "HELM_GENERIC_DEPLOY_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "HELM_GENERIC_KUBECONFIG_STATUS=⏳ Pending" + echo "HELM_GENERIC_DEPLOY_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Derive sanitized branch name id: branch @@ -156,7 +149,7 @@ runs: chmod 600 "$KCFG_PATH" echo "kubeconfig_path=${KCFG_PATH}" >> "$GITHUB_OUTPUT" echo "HELM_GENERIC_KUBECONFIG_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Kubeconfig configured::namespace: ${NAMESPACE}" + echo "βœ… Kubeconfig configured β€” namespace: ${NAMESPACE}" echo "::endgroup::" - name: Set KUBECONFIG env @@ -324,7 +317,7 @@ runs: --timeout "${HELM_TIMEOUT}" \ ${ATOMIC_ARGS[@]+"${ATOMIC_ARGS[@]}"} ${EXTRA_ARGS_ARR[@]+"${EXTRA_ARGS_ARR[@]}"} echo "HELM_GENERIC_DEPLOY_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Helm deploy complete::App: ${APP_NAME}, namespace: ${NAMESPACE}" + echo "βœ… Helm deploy complete β€” app: ${APP_NAME}, namespace: ${NAMESPACE}" echo "::endgroup::" - name: Show release status diff --git a/.github/actions/helm-package-push/action.yml b/.github/actions/helm-package-push/action.yml index 0f72eeb..ae56ea5 100644 --- a/.github/actions/helm-package-push/action.yml +++ b/.github/actions/helm-package-push/action.yml @@ -77,13 +77,13 @@ outputs: runs: using: 'composite' steps: - - name: Announce chart package and push + - name: Initialize chart package and push shell: bash run: | - echo "::notice title=☸️ [HELM] Package and Push Chart::chart: ${{ inputs.chart-name }}:${{ inputs.version }}, source: ${{ inputs.chart-path }}, publish: ${{ inputs.publish-method }}" - echo "πŸ“¦ [HELM] Destination: ${{ inputs.package-destination }}, update Chart.yaml: ${{ inputs.update-chart-yaml }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Set up Helm uses: azure/setup-helm@v5 @@ -120,7 +120,7 @@ runs: # Update existing image.repository with lowercase value sed -i "/^image:/,/^[[:space:]]*repository:/ s|repository:.*|repository: ${IMAGE_REPO_LOWER}|" "$VALUES_FILE" else - echo "Warning: image.repository not found in values.yaml" + echo "::warning title=image.repository not updated::image.repository not found in values.yaml" fi fi @@ -130,7 +130,7 @@ runs: # Update existing image.tag sed -i "/^image:/,/^[[:space:]]*tag:/ s|tag:.*|tag: \"${{ inputs.image-tag }}\"|" "$VALUES_FILE" else - echo "Warning: image.tag not found in values.yaml" + echo "::warning title=image.tag not updated::image.tag not found in values.yaml" fi fi @@ -158,7 +158,7 @@ runs: echo "Packaged chart: ${PACKAGE_FILE}" ls -la ${{ inputs.package-destination }}/ echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Chart packaged::${{ inputs.chart-name }}-${{ inputs.version }}.tgz" + echo "βœ… Chart packaged β€” ${{ inputs.chart-name }}-${{ inputs.version }}.tgz" echo "::endgroup::" - name: Validate publish configuration @@ -221,7 +221,7 @@ runs: echo "chart-url=${CHART_URL}/${{ inputs.chart-name }}:${{ inputs.version }}" >> $GITHUB_OUTPUT echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Chart pushed (OCI)::${{ inputs.chart-name }}:${{ inputs.version }} β†’ ${{ inputs.registry }}" + echo "βœ… Chart pushed (OCI) β€” ${{ inputs.chart-name }}:${{ inputs.version }} β†’ ${{ inputs.registry }}" - name: Push Helm chart to ChartMuseum if: inputs.publish-method == 'chartmuseum' @@ -250,7 +250,7 @@ runs: echo "chart-url=${CHARTMUSEUM_URL%/}" >> $GITHUB_OUTPUT echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [HELM] Chart pushed (ChartMuseum)::${{ inputs.chart-name }}:${{ inputs.version }} β†’ ${{ inputs.chartmuseum-url }}" + echo "βœ… Chart pushed (ChartMuseum) β€” ${{ inputs.chart-name }}:${{ inputs.version }} β†’ ${{ inputs.chartmuseum-url }}" - name: Report failure if: failure() diff --git a/.github/actions/ios-install-cert/action.yml b/.github/actions/ios-install-cert/action.yml index eb4355b..e08d970 100644 --- a/.github/actions/ios-install-cert/action.yml +++ b/.github/actions/ios-install-cert/action.yml @@ -14,12 +14,13 @@ inputs: runs: using: composite steps: - - name: Announce certificate installation + - name: Initialize certificate installation shell: bash run: | set -euo pipefail - echo "::notice title=πŸ” [SIGN] Install iOS Signing Certificate::Importing .p12 into temporary keychain β€” value suppressed" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Install signing certificate (.p12) into temp keychain shell: bash @@ -65,7 +66,7 @@ runs: echo "KEYCHAIN_PATH=${KEYCHAIN_PATH}" >> "$GITHUB_ENV" echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [SIGN] Certificate installed::Signing identity imported into keychain at ${KEYCHAIN_PATH}" + echo "βœ… Certificate installed β€” keychain at ${KEYCHAIN_PATH}" echo "::endgroup::" - name: Report failure diff --git a/.github/actions/ios-install-profile/action.yml b/.github/actions/ios-install-profile/action.yml index d4a5723..a8705eb 100644 --- a/.github/actions/ios-install-profile/action.yml +++ b/.github/actions/ios-install-profile/action.yml @@ -7,12 +7,13 @@ inputs: runs: using: composite steps: - - name: Announce provisioning profile installation + - name: Initialize provisioning profile installation shell: bash run: | set -euo pipefail - echo "::notice title=πŸ” [SIGN] Install iOS Provisioning Profile::Installing .mobileprovision from base64 input" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Install provisioning profile (manual) shell: bash @@ -49,7 +50,7 @@ runs: echo "IOS_PROFILE_UUID=${UUID}" >> "$GITHUB_ENV" echo "IOS_PROFILE_NAME=${NAME}" >> "$GITHUB_ENV" echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [SIGN] Profile installed::Name: ${NAME}, UUID: ${UUID}" + echo "βœ… Profile installed β€” Name: ${NAME}, UUID: ${UUID}" echo "::endgroup::" - name: Report failure diff --git a/.github/actions/setup-cloudflare-domain/action.yml b/.github/actions/setup-cloudflare-domain/action.yml index 72f32d1..4d5dae6 100644 --- a/.github/actions/setup-cloudflare-domain/action.yml +++ b/.github/actions/setup-cloudflare-domain/action.yml @@ -37,12 +37,13 @@ runs: echo "::add-mask::${{ inputs.api-token }}" echo "::add-mask::${{ inputs.account-id }}" - - name: Announce domain setup + - name: Initialize domain setup shell: bash run: | - echo "::notice title=🌐 [CF-PAGES] Setup Custom Domain::project: ${{ inputs.project-name }}, domain: ${{ inputs.domain-name }}, fail-on-error: ${{ inputs.fail-on-error }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Install jq for JSON parsing shell: bash @@ -75,7 +76,7 @@ runs: fi else echo "exists=false" >> $GITHUB_OUTPUT - echo "⚠️ Could not check existing domains, will attempt to add" + echo "::warning title=Domain check failed::Could not check existing domains, will attempt to add" fi echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" @@ -122,12 +123,12 @@ runs: if [ "${{ inputs.fail-on-error }}" = "true" ]; then exit 1 else - echo "⚠️ Continuing despite domain setup failure (fail-on-error=false)" + echo "::warning title=Domain setup skipped::Continuing despite domain setup failure (fail-on-error=false)" fi fi fi echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [CF-PAGES] Domain configured::${{ inputs.domain-name }} β†’ ${{ inputs.project-name }}" + echo "βœ… Domain configured β€” ${{ inputs.domain-name }} β†’ ${{ inputs.project-name }}" - name: Domain setup complete if: steps.check-domain.outputs.exists == 'true' diff --git a/.github/actions/setup-cloudflare-project/action.yml b/.github/actions/setup-cloudflare-project/action.yml index 06ce186..8700a28 100644 --- a/.github/actions/setup-cloudflare-project/action.yml +++ b/.github/actions/setup-cloudflare-project/action.yml @@ -46,12 +46,13 @@ runs: echo "::add-mask::${{ inputs.api-token }}" echo "::add-mask::${{ inputs.account-id }}" - - name: Announce project setup + - name: Initialize project setup shell: bash run: | - echo "::notice title=🌐 [CF-PAGES] Setup Cloudflare Pages Project::project: ${{ inputs.project-name }}, branch: ${{ inputs.production-branch }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - echo "CHECKPOINT_2_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Install jq for JSON parsing shell: bash @@ -115,7 +116,7 @@ runs: exit 1 fi echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [CF-PAGES] Project created::${{ inputs.project-name }} β†’ https://${PROJECT_URL}.pages.dev" + echo "βœ… Project created β€” ${{ inputs.project-name }} β†’ https://${PROJECT_URL}.pages.dev" - name: Project setup complete if: steps.check-project.outputs.exists == 'true' diff --git a/.github/actions/tag-github-origin/action.yml b/.github/actions/tag-github-origin/action.yml index 1d67d3d..b73a75b 100644 --- a/.github/actions/tag-github-origin/action.yml +++ b/.github/actions/tag-github-origin/action.yml @@ -7,11 +7,9 @@ runs: shell: bash run: echo "::add-mask::${{ inputs.github-token }}" - - name: Announce tag creation + - name: Initialize tag creation shell: bash - run: | - echo "::notice title=🏷️ [VERSION] Create Git Tag::Creating tag '${{ inputs.tag }}' on ${{ inputs.repository }} @ ${{ inputs.sha }}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" + run: echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" - name: Tag new version on GitHub origin run: | @@ -52,10 +50,8 @@ runs: # Check if the request was successful if [[ "$http_code" =~ ^2[0-9][0-9]$ ]]; then - echo "βœ… Successfully created tag '${{ inputs.tag }}'" - echo "Response: $response_body" + echo "βœ… Created tag '${{ inputs.tag }}' on ${{ inputs.repository }}" echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [VERSION] Tag created::Tag '${{ inputs.tag }}' on ${{ inputs.repository }}" else echo "❌ Failed to create tag. HTTP Status: $http_code" echo "Response: $response_body" diff --git a/.github/actions/xcode-build/action.yml b/.github/actions/xcode-build/action.yml index 3610649..f90758f 100644 --- a/.github/actions/xcode-build/action.yml +++ b/.github/actions/xcode-build/action.yml @@ -50,19 +50,13 @@ inputs: runs: using: composite steps: - - name: Announce Xcode build + - name: Initialize Xcode build shell: bash - env: - SCHEME: ${{ inputs.scheme }} - CONFIGURATION: ${{ inputs.configuration }} - SIGNING_STYLE: ${{ inputs.signingStyle }} - WORKSPACE: ${{ inputs.workspace }} - ARCHIVE_PATH: ${{ inputs.archivePath }} run: | set -euo pipefail - echo "::notice title=🍎 [IOS] Build and Archive::scheme: ${SCHEME}, config: ${CONFIGURATION}, signing: ${SIGNING_STYLE}" - echo "πŸ”¨ [IOS] Workspace: ${WORKSPACE}, archive: ${ARCHIVE_PATH}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Build and archive shell: bash @@ -149,12 +143,12 @@ runs: xcodebuild "${ARGS[@]}" | xcpretty --color exit "${PIPESTATUS[0]}" else - echo "WARN: xcpretty not found; falling back to raw xcodebuild output" + echo "::warning title=xcpretty missing::xcpretty not found; falling back to raw xcodebuild output" xcodebuild "${ARGS[@]}" fi echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [IOS] Archive created::${ARCHIVE_PATH}" + echo "βœ… Archive created β€” ${ARCHIVE_PATH}" echo "::endgroup::" - name: Report failure diff --git a/.github/actions/xcode-export/action.yml b/.github/actions/xcode-export/action.yml index 6ae7d3e..93fdbd5 100644 --- a/.github/actions/xcode-export/action.yml +++ b/.github/actions/xcode-export/action.yml @@ -14,17 +14,13 @@ inputs: runs: using: composite steps: - - name: Announce IPA export + - name: Initialize IPA export shell: bash - env: - ARCHIVE_PATH: ${{ inputs.archivePath }} - EXPORT_PLIST: ${{ inputs.exportOptionsPlist }} - EXPORT_PATH: ${{ inputs.exportPath }} run: | set -euo pipefail - echo "::notice title=🍎 [IOS] Export IPA::Exporting archive to IPA β€” archive: ${ARCHIVE_PATH}, output: ${EXPORT_PATH}" - echo "πŸ“‹ [IOS] Export options: ${EXPORT_PLIST}" - echo "CHECKPOINT_1_STATUS=⏳ Pending" >> "$GITHUB_ENV" + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" - name: Export IPA shell: bash @@ -46,7 +42,7 @@ runs: -exportOptionsPlist "${EXPORT_PLIST}" \ -exportPath "${EXPORT_PATH}" echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" - echo "::notice title=βœ… [IOS] IPA exported::Output: ${EXPORT_PATH}" + echo "βœ… IPA exported β€” Output: ${EXPORT_PATH}" echo "::endgroup::" - name: Report failure diff --git a/.github/workflows/reusable-build-and-publish-artifacts.yml b/.github/workflows/reusable-build-and-publish-artifacts.yml new file mode 100644 index 0000000..f5c7f6e --- /dev/null +++ b/.github/workflows/reusable-build-and-publish-artifacts.yml @@ -0,0 +1,650 @@ +# ============================================================================= +# Reusable Build and Publish Artifacts +# ============================================================================= +# +# PURPOSE: +# Full CI/CD pipeline for services that ship a Docker image and a Helm chart. +# Computes a semantic version, optionally publishes NuGet packages, builds and +# pushes a Docker image to a configurable registry, packages and publishes the +# Helm chart to GitHub Container Registry (GHCR) as an OCI artifact, and only +# then tags the Git origin. Called by per-service caller workflows via +# `workflow_call`. +# +# USAGE: +# jobs: +# publish: +# uses: simplify9/.github/.github/workflows/reusable-build-and-publish-artifacts.yml@main +# with: +# chart-name: my-service # required β€” must match Chart.yaml name: +# major-version: '2' +# minor-version: '1' +# secrets: +# registry-password: ${{ secrets.GITHUB_TOKEN }} +# nuget-api-key: ${{ secrets.NUGET_API_KEY }} # omit to skip NuGet +# +# INPUTS: +# Required: +# chart-name string Helm chart name. Must exactly match the +# name: field in Chart.yaml β€” Helm derives +# the package filename from Chart.yaml, not +# this input. +# +# Optional β€” versioning: +# major-version string SemVer major component. (default: '1') +# minor-version string SemVer minor component. (default: '0') +# +# Optional β€” .NET / NuGet: +# dotnet-version string .NET SDK version to install. (default: '8.0.x') +# nuget-projects string Glob of .csproj files to pack +# and push. Leave empty to skip +# NuGet publishing entirely. (default: '') +# test-projects string Glob of test .csproj files. (default: '**/*UnitTests/*.csproj') +# run-tests string Set to 'true' to run tests. (default: 'false') +# +# Optional β€” Docker: +# dockerfile-path string Path to the Dockerfile. (default: './Dockerfile') +# docker-context string Docker build context directory. (default: '.') +# docker-platforms string Comma-separated target platform(s). +# (default: 'linux/amd64') +# +# Optional β€” Helm: +# chart-path string Path to the Helm chart directory. +# (default: './chart') +# +# Optional β€” Registry: +# container-registry string Registry for the Docker image. (default: 'ghcr.io') +# image-name string Full Docker image name. +# Defaults to github.repository. +# +# SECRETS: +# Optional: +# nuget-api-key API key for NuGet push. +# nuget-source NuGet feed URL. Defaults to nuget.org v3. +# registry-username Username for the Docker image registry. +# registry-password Password/token for the Docker image registry. +# github-token PAT for git tagging; falls back to GITHUB_TOKEN. +# +# Note: Helm chart publishing to GHCR always uses github.actor + +# GITHUB_TOKEN and is deliberately decoupled from registry-username / +# registry-password, which may target a non-GHCR image registry. +# +# OUTPUTS: +# version Computed semantic version string, e.g. '1.2.57'. +# docker-image Full Docker image reference(s) including tag. On the +# default branch this contains TWO newline-separated tags +# (the version and 'latest'); on other refs, just the version. +# helm-chart OCI URL of the published Helm chart, e.g.: +# oci://ghcr.io//charts/: +# +# PERMISSIONS: +# The workflow defaults to `contents: read` and grants each job only what it +# needs (least privilege): +# version contents: read Reads git tags to compute semver. +# nuget contents: read, packages: write Optional GitHub Packages feed. +# ci contents: read, packages: write Pushes image + chart to GHCR. +# tag contents: write Creates the git tag, only after +# a successful build. +# +# SECURITY: This template holds `packages: write` and consumes secrets. +# Only invoke it from trusted events (push to a protected branch or tag). +# Never wire a caller to `pull_request_target` or fork `pull_request` +# triggers β€” doing so would expose write tokens and secrets to untrusted code. +# +# CONCURRENCY: +# Runs are serialized per ref so concurrent pushes cannot compute the same +# patch version and collide when tagging. In-flight publishes are never +# cancelled (cancel-in-progress: false) to avoid interrupting a registry push. +# +# JOBS: +# version Computes semver via determine-semver from existing git tags. +# nuget (skipped when nuget-projects is empty) Restores, builds, +# and packs .NET projects; pushes to the NuGet feed. +# ci Validates chart config, builds and pushes the Docker +# β”‚ image, packages the Helm chart, and pushes the OCI +# β”‚ artifact to GHCR. Four inline checkpoints: +# β”‚ 1 Config validated chart-name non-empty, chart-path exists, +# β”‚ Chart.yaml present, name: matches input. +# β”‚ 2 Docker image built Image pushed to container-registry. +# β”‚ 3 Helm chart packaged .tgz written to ./helm-packages/. +# β”” 4 Helm chart pushed OCI artifact live in GHCR. +# tag Creates and pushes the git tag β€” only after version, ci, +# and (when present) nuget succeed, so tags never point at +# a version that was never published. +# +# DEPENDENCIES: +# Composite actions (simplify9/.github): +# determine-semver@main Computes next SemVer from existing git tags. +# tag-github-origin@main Creates a git tag via the REST API (tag job). +# dotnet-build@main Restores, builds, and optionally tests .NET projects. +# dotnet-pack-push@main Packs and pushes NuGet packages. +# docker-build-push@main Builds multi-platform Docker image and pushes it. +# External actions: +# actions/checkout@v6 Standard repository checkout. +# azure/setup-helm@v5 Installs the Helm CLI (latest stable). +# ============================================================================= + +name: Reusable Build and Publish Artifacts + +on: + workflow_call: + inputs: + # Version configuration + major-version: + description: 'Major version number' + required: false + default: '1' + type: string + minor-version: + description: 'Minor version number' + required: false + default: '0' + type: string + + # .NET Build configuration + dotnet-version: + description: 'Dotnet version to use' + required: false + default: '8.0.x' + type: string + nuget-projects: + description: 'NuGet projects to pack and push (glob pattern). Leave empty to skip NuGet publishing.' + required: false + default: '' + type: string + test-projects: + description: 'Test projects to run (glob pattern)' + required: false + default: '**/*UnitTests/*.csproj' + type: string + run-tests: + description: 'Whether to run tests' + required: false + default: 'false' + type: string + + # Docker configuration + dockerfile-path: + description: 'Path to Dockerfile' + required: false + default: './Dockerfile' + type: string + docker-context: + description: 'Docker build context' + required: false + default: '.' + type: string + docker-platforms: + description: 'Target platforms for Docker build' + required: false + default: 'linux/amd64' + type: string + + # Helm configuration + chart-path: + description: 'Path to Helm chart directory' + required: false + default: './chart' + type: string + chart-name: + description: 'Helm chart name' + required: true + type: string + + # Registry configuration + container-registry: + description: 'Container registry (docker.io, ghcr.io, etc.)' + required: false + default: 'ghcr.io' + type: string + image-name: + description: 'Docker image name (will use repository name if not provided)' + required: false + type: string + + secrets: + # NuGet secrets + nuget-api-key: + description: 'NuGet API key' + required: false + nuget-source: + description: 'NuGet source URL' + required: false + # Registry secrets + registry-username: + description: 'Container registry username' + required: false + registry-password: + description: 'Container registry password/token' + required: false + + # GitHub token for tagging + github-token: + description: 'GitHub token for tagging' + required: false + + outputs: + version: + description: 'Generated semantic version' + value: ${{ jobs.version.outputs.version }} + docker-image: + description: 'Built Docker image with tag' + value: ${{ jobs.ci.outputs.docker-image }} + helm-chart: + description: 'Published Helm chart URL' + value: ${{ jobs.ci.outputs.helm-chart }} + +# Default to least privilege; each job escalates only what it needs. +permissions: + contents: read + +# Serialize runs per ref so concurrent pushes cannot race the tag-based +# semver computation. Never cancel an in-flight publish mid-push. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +env: + # Set default registry and image name + CONTAINER_REGISTRY: ${{ inputs.container-registry }} + IMAGE_NAME: ${{ inputs.image-name || github.repository }} + +jobs: + version: + runs-on: ubuntu-latest + timeout-minutes: 10 + permissions: + contents: read + outputs: + version: ${{ steps.semver.outputs.version }} + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Determine semantic version + id: semver + uses: simplify9/.github/.github/actions/determine-semver@main + with: + major: ${{ inputs.major-version }} + minor: ${{ inputs.minor-version }} + + - name: Write version job summary + if: always() + env: + JOB_STATUS: ${{ job.status }} + SEMVER_VERSION: ${{ steps.semver.outputs.version }} + REF_NAME: ${{ github.ref_name }} + COMMIT_SHA: ${{ github.sha }} + RUNNER_OS: ${{ runner.os }} + ACTOR: ${{ github.actor }} + run: | + STATUS=$( [ "${JOB_STATUS}" = "success" ] \ + && echo "βœ… SUCCESS" || echo "❌ FAILED" ) + { + echo "## 🏷️ Version Job β€” ${STATUS}" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Computed version | \`${SEMVER_VERSION}\` |" + echo "| Branch | \`${REF_NAME}\` |" + echo "| SHA | \`${COMMIT_SHA}\` |" + echo "| Runner | ${RUNNER_OS} |" + echo "| Triggered by | ${ACTOR} |" + } >> "$GITHUB_STEP_SUMMARY" + + nuget: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: version + if: ${{ inputs.nuget-projects != '' }} + permissions: + contents: read + packages: write # in case nuget-source targets a GitHub Packages feed + steps: + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Dotnet Restore, Build, and Test + uses: simplify9/.github/.github/actions/dotnet-build@main + with: + projects: "**/*.csproj" + test-projects: ${{ inputs.test-projects }} + configuration: "Release" + dotnet-version: ${{ inputs.dotnet-version }} + run-tests: ${{ inputs.run-tests }} + + - name: Pack and Push NuGet Package + uses: simplify9/.github/.github/actions/dotnet-pack-push@main + with: + projects: ${{ inputs.nuget-projects }} + configuration: "Release" + version: ${{ needs.version.outputs.version }} + api-key: ${{ secrets.nuget-api-key }} + nuget-source: ${{ secrets.nuget-source || 'https://api.nuget.org/v3/index.json' }} + + - name: Write nuget job summary + if: always() + env: + JOB_STATUS: ${{ job.status }} + VERSION: ${{ needs.version.outputs.version }} + NUGET_PROJECTS: ${{ inputs.nuget-projects }} + REF_NAME: ${{ github.ref_name }} + RUNNER_OS: ${{ runner.os }} + run: | + STATUS=$( [ "${JOB_STATUS}" = "success" ] \ + && echo "βœ… SUCCESS" || echo "❌ FAILED" ) + { + echo "## πŸ”· NuGet Publish Job β€” ${STATUS}" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Version | \`${VERSION}\` |" + echo "| Projects | \`${NUGET_PROJECTS}\` |" + echo "| Branch | \`${REF_NAME}\` |" + echo "| Runner | ${RUNNER_OS} |" + } >> "$GITHUB_STEP_SUMMARY" + + ci: + runs-on: ubuntu-latest + timeout-minutes: 60 + needs: version + permissions: + contents: read + packages: write + outputs: + docker-image: ${{ steps.docker.outputs.image-tags }} + helm-chart: ${{ steps.helm_oci.outputs.chart-url }} + steps: + - name: Initialize CI run + env: + VERSION: ${{ needs.version.outputs.version }} + REGISTRY: ${{ inputs.container-registry }} + run: | + echo "::group::🐳 CI build context" + printf '%-12s%s\n' 'Version' "${VERSION}" + printf '%-12s%s\n' 'Registry' "${REGISTRY}" + echo "::endgroup::" + # Seed checkpoint state; each stage flips its own entry to PASSED. + { + echo "CHECKPOINT_1_STATUS=⏳ Pending" + echo "CHECKPOINT_2_STATUS=⏳ Pending" + echo "CHECKPOINT_3_STATUS=⏳ Pending" + echo "CHECKPOINT_4_STATUS=⏳ Pending" + } >> "$GITHUB_ENV" + + - name: Checkout code + uses: actions/checkout@v6 + with: + persist-credentials: false + + - name: Validate chart publish configuration + env: + CHART_PATH: ${{ inputs.chart-path }} + CHART_NAME: ${{ inputs.chart-name }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + run: | + echo "::group::🐳 [CHECKPOINT 1/4] Validate chart publish configuration" + + if [ -z "${CHART_NAME}" ]; then + echo "Error: chart-name input is required." + exit 1 + fi + + if [ ! -d "${CHART_PATH}" ]; then + echo "Error: chart-path '${CHART_PATH}' does not exist." + exit 1 + fi + + if [ ! -f "${CHART_PATH}/Chart.yaml" ]; then + echo "Error: Chart.yaml not found at '${CHART_PATH}/Chart.yaml'." + exit 1 + fi + + CHART_YAML_NAME="$(yq '.name // ""' "${CHART_PATH}/Chart.yaml")" + + if [ -z "${CHART_YAML_NAME}" ]; then + echo "Error: Chart.yaml does not contain a top-level name field." + exit 1 + fi + + if [ "${CHART_YAML_NAME}" != "${CHART_NAME}" ]; then + echo "Error: chart-name input '${CHART_NAME}' does not match Chart.yaml name '${CHART_YAML_NAME}'." + echo "Helm package filenames are derived from Chart.yaml name, so these must match." + exit 1 + fi + + if [ ! -f "${CHART_PATH}/values.yaml" ]; then + echo "::warning title=Helm values.yaml missing::No values.yaml at '${CHART_PATH}'; image repository/tag will not be updated." + fi + + OWNER_LOWER="$(echo "${REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" + printf '%-16s%s\n' 'Chart path' "${CHART_PATH}" + printf '%-16s%s\n' 'Chart name' "${CHART_NAME}" + printf '%-16s%s\n' 'Chart.yaml name' "${CHART_YAML_NAME}" + printf '%-16s%s\n' 'Publish target' "oci://ghcr.io/${OWNER_LOWER}/charts" + echo "CHECKPOINT_1_STATUS=βœ… PASSED" >> "$GITHUB_ENV" + echo "βœ… [1/4] Chart configuration validated" + echo "::endgroup::" + + - name: Build and push Docker image + id: docker + uses: simplify9/.github/.github/actions/docker-build-push@main + with: + registry: ${{ env.CONTAINER_REGISTRY }} + image-name: ${{ env.IMAGE_NAME }} + version: ${{ needs.version.outputs.version }} + username: ${{ secrets.registry-username || github.actor }} + password: ${{ secrets.registry-password || secrets.GITHUB_TOKEN }} + dockerfile: ${{ inputs.dockerfile-path }} + context: ${{ inputs.docker-context }} + platforms: ${{ inputs.docker-platforms }} + + - name: Confirm Docker image built + run: | + echo "CHECKPOINT_2_STATUS=βœ… PASSED" >> "$GITHUB_ENV" + echo "βœ… [2/4] Docker image built and pushed" + + - name: Set up Helm + uses: azure/setup-helm@v5 + with: + version: latest + + - name: Prepare Helm chart metadata + shell: bash + env: + CHART_PATH: ${{ inputs.chart-path }} + VERSION: ${{ needs.version.outputs.version }} + CONTAINER_REGISTRY: ${{ env.CONTAINER_REGISTRY }} + IMAGE_NAME: ${{ env.IMAGE_NAME }} + run: | + set -euo pipefail + + echo "::group::☸️ Prepare Helm chart metadata" + + # Lowercase for OCI/registry correctness; export for yq's strenv(). + IMAGE_REPOSITORY="$(echo "${CONTAINER_REGISTRY}/${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')" + export IMAGE_REPOSITORY VERSION + + # yq edits YAML structurally (creating appVersion if absent), which is + # far more robust than line-oriented sed against arbitrary formatting. + yq -i '.version = strenv(VERSION) | .appVersion = strenv(VERSION)' "${CHART_PATH}/Chart.yaml" + printf '%-18s%s\n' 'Chart version' "${VERSION}" + printf '%-18s%s\n' 'appVersion' "${VERSION}" + + VALUES_FILE="${CHART_PATH}/values.yaml" + if [ -f "$VALUES_FILE" ]; then + if yq -e '.image' "$VALUES_FILE" >/dev/null 2>&1; then + yq -i '.image.repository = strenv(IMAGE_REPOSITORY) | .image.tag = strenv(VERSION)' "$VALUES_FILE" + printf '%-18s%s\n' 'image.repository' "${IMAGE_REPOSITORY}" + printf '%-18s%s\n' 'image.tag' "${VERSION}" + else + echo "::warning title=Helm image block missing::No top-level 'image:' in values.yaml; skipping image value update." + fi + fi + + echo "::endgroup::" + + - name: Package Helm chart + id: helm_package + shell: bash + env: + CHART_PATH: ${{ inputs.chart-path }} + CHART_NAME: ${{ inputs.chart-name }} + VERSION: ${{ needs.version.outputs.version }} + run: | + set -euo pipefail + + echo "::group::☸️ [CHECKPOINT 3/4] Package Helm Chart" + + PACKAGE_DESTINATION="./helm-packages" + mkdir -p "$PACKAGE_DESTINATION" + + if grep -q "^dependencies:" "${CHART_PATH}/Chart.yaml"; then + helm dependency update "$CHART_PATH" + fi + + # Fail fast on a malformed chart before packaging/publishing. + helm lint "$CHART_PATH" + + helm package "$CHART_PATH" --destination "$PACKAGE_DESTINATION" + + PACKAGE_FILE="${PACKAGE_DESTINATION}/${CHART_NAME}-${VERSION}.tgz" + + if [ ! -f "$PACKAGE_FILE" ]; then + echo "Error: Expected chart package not found: $PACKAGE_FILE" + echo "Available packages:" + ls -la "$PACKAGE_DESTINATION" + exit 1 + fi + + echo "package-file=${PACKAGE_FILE}" >> "$GITHUB_OUTPUT" + echo "CHECKPOINT_3_STATUS=βœ… PASSED" >> "$GITHUB_ENV" + + echo "βœ… [3/4] Helm chart packaged: ${PACKAGE_FILE}" + echo "::endgroup::" + + - name: Push Helm chart to GHCR OCI + id: helm_oci + shell: bash + env: + REGISTRY_USERNAME: ${{ github.actor }} + REGISTRY_PASSWORD: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + CHART_NAME: ${{ inputs.chart-name }} + VERSION: ${{ needs.version.outputs.version }} + PACKAGE_FILE: ${{ steps.helm_package.outputs.package-file }} + run: | + set -euo pipefail + + echo "::group::☸️ [CHECKPOINT 4/4] Push Helm Chart to GHCR OCI" + + echo "::add-mask::${REGISTRY_PASSWORD}" + + OWNER_LOWER="$(echo "${REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" + CHART_REPOSITORY="ghcr.io/${OWNER_LOWER}/charts" + CHART_PUSH_URL="oci://${CHART_REPOSITORY}" + CHART_FULL_URL="${CHART_PUSH_URL}/${CHART_NAME}:${VERSION}" + + echo "Authenticating Helm to GHCR..." + echo "$REGISTRY_PASSWORD" | helm registry login ghcr.io \ + --username "$REGISTRY_USERNAME" \ + --password-stdin + + echo "Pushing chart package: ${PACKAGE_FILE}" + echo "Destination: ${CHART_PUSH_URL}" + + helm push "$PACKAGE_FILE" "$CHART_PUSH_URL" + + echo "chart-url=${CHART_FULL_URL}" >> "$GITHUB_OUTPUT" + echo "CHECKPOINT_4_STATUS=βœ… PASSED" >> "$GITHUB_ENV" + + echo "βœ… [4/4] Helm chart pushed" + echo "::endgroup::" + + # The one run-level success annotation: a pointer to the published artifact. + echo "::notice title=Published ${CHART_NAME} ${VERSION}::${CHART_FULL_URL}" + + - name: Report ci job failure + if: failure() + env: + CHART_NAME: ${{ inputs.chart-name }} + VERSION: ${{ needs.version.outputs.version }} + run: | + # Single error annotation surfaces the failure at the top of the run; + # the checkpoint breakdown stays in the (collapsible) log for triage. + echo "::error title=CI build failed for ${CHART_NAME} ${VERSION}::Failed before completing all 4 checkpoints β€” see the run log for the first ❌." + echo "::group::Checkpoint breakdown" + printf '%-26s%s\n' '1 β€” Config validation' "${CHECKPOINT_1_STATUS:-❌ Not reached}" + printf '%-26s%s\n' '2 β€” Docker build' "${CHECKPOINT_2_STATUS:-❌ Not reached}" + printf '%-26s%s\n' '3 β€” Helm chart packaged' "${CHECKPOINT_3_STATUS:-❌ Not reached}" + printf '%-26s%s\n' '4 β€” Helm chart pushed' "${CHECKPOINT_4_STATUS:-❌ Not reached}" + echo "::endgroup::" + + - name: Write ci job summary + if: always() + env: + JOB_STATUS: ${{ job.status }} + CHART_NAME: ${{ inputs.chart-name }} + VERSION: ${{ needs.version.outputs.version }} + REGISTRY_INPUT: ${{ inputs.container-registry }} + CHART_URL: ${{ steps.helm_oci.outputs.chart-url }} + REPOSITORY_OWNER: ${{ github.repository_owner }} + REF_NAME: ${{ github.ref_name }} + RUNNER_OS: ${{ runner.os }} + run: | + STATUS=$( [ "${JOB_STATUS}" = "success" ] \ + && echo "βœ… SUCCESS" || echo "❌ FAILED" ) + OWNER_LOWER="$(echo "${REPOSITORY_OWNER}" | tr '[:upper:]' '[:lower:]')" + { + echo "## 🐳 CI Build β€” ${CHART_NAME} β€” ${STATUS}" + echo "" + echo "| Field | Value |" + echo "|-------|-------|" + echo "| Version | \`${VERSION}\` |" + echo "| Registry | \`${REGISTRY_INPUT}\` |" + echo "| Chart registry | \`oci://ghcr.io/${OWNER_LOWER}/charts\` |" + echo "| Branch | \`${REF_NAME}\` |" + echo "| Runner | ${RUNNER_OS} |" + echo "" + echo "## πŸ“‹ Checkpoint Summary" + echo "" + echo "| # | Checkpoint | Status |" + echo "|---|------------|--------|" + echo "| 1 | Config validated | ${CHECKPOINT_1_STATUS:-⏭️ Not reached} |" + echo "| 2 | Docker image built | ${CHECKPOINT_2_STATUS:-⏭️ Not reached} |" + echo "| 3 | Helm chart packaged | ${CHECKPOINT_3_STATUS:-⏭️ Not reached} |" + echo "| 4 | Helm chart pushed | ${CHECKPOINT_4_STATUS:-⏭️ Not reached} |" + if [ -n "${CHART_URL}" ]; then + echo "" + echo "## ☸️ Install" + echo "" + echo "\`\`\`bash" + echo "helm upgrade --install ${CHART_NAME} oci://ghcr.io/${OWNER_LOWER}/charts/${CHART_NAME} --version ${VERSION}" + echo "\`\`\`" + echo "" + echo "> **Note:** If this is the first time publishing this package to GHCR, verify the package visibility is set to **Public** in GitHub Packages settings. GHCR packages are not guaranteed to become public automatically on first push." + fi + } >> "$GITHUB_STEP_SUMMARY" + + tag: + runs-on: ubuntu-latest + timeout-minutes: 10 + needs: [version, nuget, ci] + # Tag only after a successful build so tags never reference an unshipped + # version. A skipped nuget job (no nuget-projects) must not block tagging. + if: >- + ${{ !cancelled() + && needs.version.result == 'success' + && needs.ci.result == 'success' + && (needs.nuget.result == 'success' || needs.nuget.result == 'skipped') }} + permissions: + contents: write + steps: + - name: Tag new version on GitHub origin + uses: simplify9/.github/.github/actions/tag-github-origin@main + with: + github-token: ${{ secrets.github-token || secrets.GITHUB_TOKEN }} + repository: ${{ github.repository }} + tag: ${{ needs.version.outputs.version }} + sha: ${{ github.sha }}