name: Publish Helm Chart with patched Application Version on: workflow_dispatch: inputs: chart-location: required: false type: string default: "." description: "location of the helm chart directory" chart-name: required: true type: string description: "name of the helm chart" workflow_call: inputs: chart-location: required: false type: string default: "." description: "location of the helm chart directory" chart-name: required: true type: string description: "name of the helm chart" outputs: chart-version: description: "the version of the published helm chart" value: ${{ jobs.publish-helm-chart.outputs.chart-version }} secrets: OP_CONNECT_TOKEN: required: true jobs: publish-helm-chart: outputs: chart-version: ${{ steps.get-chart-version.outputs.CHART_VERSION }} runs-on: ubuntu-latest steps: - name: "Checkout Repository" uses: actions/checkout@v7 - name: "Install Helm" uses: azure/setup-helm@v5 - name: "Configure OP-Connect" uses: 1password/load-secrets-action/configure@v5 with: connect-host: https://op-connect.jk-dev.de # helm package exits 1 with "found in Chart.yaml, but missing in charts/ # directory" while a declared dependency has not been fetched, so this # comes first. It is a no-op for every chart without a dependencies: # block; ci/helm-dependencies.sh explains why it resolves from Chart.yaml # instead of a committed Chart.lock. Note the consequence for the # published artifact: the subchart .tgz is packaged INTO ours, so the # version pinned in Chart.yaml is the version operators get, and nothing # downstream ever sees the dependency again. - name: "Fetch chart dependencies" run: ${{ github.workspace }}/ci/helm-dependencies.sh "${{ inputs.chart-name }}" working-directory: ${{ inputs.chart-location }} - name: "Package Helm Chart" run: helm package ${{ inputs.chart-name }} working-directory: ${{ inputs.chart-location }} - name: "Get Helm Chart Version" id: get-chart-version run: |- CHART_VERSION=$(grep '^version:' ${{ inputs.chart-name}}/Chart.yaml | awk '{print $2}') echo $CHART_VERSION echo "CHART_VERSION=$CHART_VERSION" >> $GITHUB_OUTPUT working-directory: ${{ inputs.chart-location }} - name: "Load Chart Registry Credentials" id: load-registry-creds uses: 1password/load-secrets-action@v5 env: OP_CONNECT_TOKEN: ${{ secrets.OP_CONNECT_TOKEN }} REGISTRY_PASSWORD: op://GitHub-Actions/jlab-registry-access/password REGISTRY_USERNAME: op://GitHub-Actions/jlab-registry-access/username REGISTRY_CHARTS_URL: op://GitHub-Actions/jlab-registry-access/charts-url # The target is a classic, index-based Helm registry, not an OCI one, so # there is no "helm registry login" session and no "helm push": the upload # is a single HTTP POST of the packaged .tgz to /api/charts, # authenticated per request. Three properties of this step are load-bearing # and each of them was paid for once already. # # 1) The values are mapped into the step's own env block and read as quoted # shell variables - never interpolated into the command line. load-secrets- # action v5 (bumped in #140) no longer exports the loaded values as # environment variables for later steps the way v4 did; only the step # outputs carry them. Every "${{ env.* }}" reference therefore resolved to # an empty string from that bump on, with nothing reporting an error - the # loading step logs "Populating variable" and stays green either way. The # ":?" guards fail loudly and name the op:// field that is missing, and # quoting keeps an empty value from silently shifting arguments. # # 2) The credentials never reach the process list. "curl --user $U:$P" # would put them in argv; "curl --config -" reads its options from stdin # instead, so nothing sensitive appears in any argument. The price of that # config file is its value syntax: an unquoted value ends at the first # space (measured on curl 8.5.0 - "has space" arrived as "has"), so the # step refuses whitespace in the credentials up front rather than letting # a truncated password come back as a puzzling 401. A Gitea access token # is 40 hex characters, so this only ever fires on a mis-entered item. # # 3) "--fail-with-body" is what makes a failed upload fail the job. Unlike # "helm push", curl exits 0 on HTTP 401/403/409 unless asked not to - the # publish would go green while nothing was uploaded. Measured against Gitea # 1.24.7: a 401 without the flag is exit 0, with the flag exit 22, and the # response body is still printed so the log names the cause. # # The file name is built from the chart name and the version read above # instead of a "-*.tgz" glob: --upload-file takes exactly one file, # and a glob matching two turns the second into curl's URL argument, while # a glob matching none is passed through literally. Both fail obscurely. # "helm package" keeps the "+" of a "1.4.0+up1.9.2" version verbatim in the # file name, so "-.tgz" is an exact match (measured). - name: "Upload Helm Chart to the Chart Registry" env: REGISTRY_PASSWORD: ${{ steps.load-registry-creds.outputs.REGISTRY_PASSWORD }} REGISTRY_USERNAME: ${{ steps.load-registry-creds.outputs.REGISTRY_USERNAME }} REGISTRY_CHARTS_URL: ${{ steps.load-registry-creds.outputs.REGISTRY_CHARTS_URL }} CHART_NAME: ${{ inputs.chart-name }} CHART_VERSION: ${{ steps.get-chart-version.outputs.CHART_VERSION }} run: |- : "${REGISTRY_CHARTS_URL:?empty - check op://GitHub-Actions/jlab-registry-access/charts-url}" : "${REGISTRY_USERNAME:?empty - check op://GitHub-Actions/jlab-registry-access/username}" : "${REGISTRY_PASSWORD:?empty - check op://GitHub-Actions/jlab-registry-access/password}" : "${CHART_NAME:?empty - the chart-name input did not arrive}" : "${CHART_VERSION:?empty - could not read version: from ${CHART_NAME}/Chart.yaml}" # 1Password masks the full URL, but a curl connection error prints only # ":", which that mask would not cover. Mask the host too. CHARTS_HOST="${REGISTRY_CHARTS_URL#*://}" echo "::add-mask::${CHARTS_HOST%%/*}" case "${REGISTRY_USERNAME}${REGISTRY_PASSWORD}" in *[[:space:]]*) echo "::error::username/password in op://GitHub-Actions/jlab-registry-access must not contain whitespace - curl --config truncates an unquoted value at the first space" exit 1 ;; esac CHART_PACKAGE="${CHART_NAME}-${CHART_VERSION}.tgz" test -f "$CHART_PACKAGE" || { echo "::error::${CHART_PACKAGE} does not exist - helm package produced: $(ls -1 ./*.tgz 2>&1)" exit 1 } echo "uploading ${CHART_PACKAGE}" printf 'user = %s:%s\nurl = %s\n' \ "$REGISTRY_USERNAME" "$REGISTRY_PASSWORD" "${REGISTRY_CHARTS_URL%/}/api/charts" | curl --config - \ --fail-with-body \ --silent --show-error \ --request POST \ --upload-file "$CHART_PACKAGE" \ --write-out '\nHTTP %{http_code}\n' working-directory: ${{ inputs.chart-location }}