Der Workflow kommt aus jlab-cluster/helm-charts, wo er 26 duenne Huellen bedient. Er laedt ein gepacktes Chart in eine klassische, indexbasierte Helm-Registry: ein einzelnes HTTP-POST, kein OCI-helm-push. Zwei Dinge sind beim Umzug anders als in der Vorlage. Der Eigentuemer ist eine Pflichteingabe registry-owner vom Typ string, ohne Vorgabe. Bisher wurde er aus github.repository_owner abgeleitet; als Eingabe wird er im aufrufenden Repository ausgewertet, wo der Wert gemessen ist. Das folgt der Hausregel, dass ein Eigentuemer-Segment nie geraten wird, und haengt den Baustein an keine Kontext-Semantik. type: string wie alle Eingaben hier, siehe #4. OP_CONNECT_HOST kommt aus einer Variablen des aufrufenden Repositories statt fest verdrahtet aus dem Baustein — ein geteilter Baustein traegt nicht die Infrastruktur-Adresse eines einzelnen Mandanten. Ein erster Schritt prueft die Variable und benennt sie beim Abbruch, statt zwei Schritte spaeter an einer leeren Adresse zu scheitern. Alles andere ist unveraendert uebernommen, samt der Kommentare, die es begruenden: curl --config - statt --user, --fail-with-body, der gebaute Dateiname mit test -f, die :?-Waechter je Tresor-Feld, die Leerzeichen-Pruefung an den Zugangsdaten, das Maskieren des Hosts und die drei Formwaechter an charts-url. Der workflow_dispatch-Block ist entfallen: Ein Dispatch hier liefe gegen dieses Repository, in dem kein Chart liegt. Die Huellen des Aufrufers tragen ihn. ci/helm-dependencies.sh bleibt ein Skript des aufrufenden Repositories; der Checkout holt dessen Baum, also laeuft es weiter aus dessen Kopie. Part of jlab-cluster/helm-charts#66
250 lines
14 KiB
YAML
250 lines
14 KiB
YAML
name: Publish Helm Chart with patched Application Version
|
|
|
|
# Called through "uses:" only, never triggered here. The thin wrappers in the
|
|
# calling repository carry their own "workflow_dispatch" and "on: push", so the
|
|
# workflow_dispatch block this file used to have is gone: a dispatch in THIS
|
|
# repository would run against this repository's files, where there is no chart.
|
|
#
|
|
# The comments below are kept from the copy this file replaces, in English as
|
|
# they were written. Each of them records something that was paid for once -
|
|
# none of it is restating what the YAML already says.
|
|
on:
|
|
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"
|
|
# Deliberately required and without a default, following the house rule
|
|
# of this repository: an owner segment is never guessed. The caller
|
|
# passes "${{ github.repository_owner }}", where the value is measured
|
|
# (jlab-cluster, run 282 of validate-charts.yml) - inside a workflow
|
|
# called from another repository it is documented to stay the caller's
|
|
# owner, but that is not measured on this Gitea version, and a published
|
|
# chart landing under the wrong owner is not worth relying on a context
|
|
# semantic for. As an input the value is evaluated in the caller, where
|
|
# it is known to be right.
|
|
#
|
|
# type: string, like every input here. On this Gitea version (measured on
|
|
# 1.27.0, issue #4) a boolean workflow_call input does not reach the
|
|
# called workflow at all - neither its default nor an explicitly passed
|
|
# value - while the run stays green. Every input of this block is
|
|
# therefore a string, even one that is a switch in meaning.
|
|
registry-owner:
|
|
required: true
|
|
type: string
|
|
description: >-
|
|
Owner segment of the chart registry URL, i.e. the <owner> in
|
|
<base>/<owner>/helm/api/charts. Deliberately has no default: an owner
|
|
segment is never guessed. Callers pass github.repository_owner, which
|
|
resolves in the calling repository, where the value is measured.
|
|
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
|
|
# The Connect host is a variable of the CALLING repository, not a value
|
|
# wired into this file: a shared building block must not carry one
|
|
# tenant's infrastructure address. The price of that is a prerequisite
|
|
# that can be forgotten, and an unset variable arrives as an empty
|
|
# string. An empty connect-host does not fail here - it fails two steps
|
|
# later, inside the loading action, against a URL nobody can see. So the
|
|
# guard comes first and names the setting, the place to set it and the
|
|
# shape of the value.
|
|
- name: "Check the calling repository's prerequisites"
|
|
env:
|
|
OP_CONNECT_HOST: ${{ vars.OP_CONNECT_HOST }}
|
|
run: |-
|
|
if [ -z "$OP_CONNECT_HOST" ]; then
|
|
echo "::error::the variable OP_CONNECT_HOST is not set in the calling repository (Settings -> Actions -> Variables). It must hold the address of the 1Password Connect server, e.g. https://op-connect.example.com. This workflow deliberately does not hardcode a host."
|
|
exit 1
|
|
fi
|
|
- name: "Configure OP-Connect"
|
|
uses: 1password/load-secrets-action/configure@v5
|
|
with:
|
|
connect-host: ${{ vars.OP_CONNECT_HOST }}
|
|
# 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.
|
|
#
|
|
# The script belongs to the CALLING repository and that is unchanged by
|
|
# the move: the checkout above clones the repository the run belongs to -
|
|
# the caller - so ${{ github.workspace }} is the caller's tree and the
|
|
# script is the caller's copy. It is therefore a prerequisite of every
|
|
# caller, the same way OP_CONNECT_HOST is, and the guard below says so
|
|
# rather than letting the shell report a missing file.
|
|
- name: "Fetch chart dependencies"
|
|
env:
|
|
HELM_DEPENDENCIES_SCRIPT: ${{ github.workspace }}/ci/helm-dependencies.sh
|
|
CHART_NAME: ${{ inputs.chart-name }}
|
|
run: |-
|
|
test -x "$HELM_DEPENDENCIES_SCRIPT" || {
|
|
echo "::error::${HELM_DEPENDENCIES_SCRIPT} is missing or not executable - ci/helm-dependencies.sh is a script of the calling repository and this workflow runs it before helm package"
|
|
exit 1
|
|
}
|
|
"$HELM_DEPENDENCIES_SCRIPT" "$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
|
|
# <charts-url>/<owner>/helm/api/charts, authenticated per request.
|
|
#
|
|
# That URL is composed, not stored (jlab-cluster/helm-charts#63).
|
|
# charts-url holds only the base up to "/api/packages"; the owner segment
|
|
# comes from the registry-owner input, which the caller fills with
|
|
# github.repository_owner - measured as "jlab-cluster" in run 282 of
|
|
# validate-charts.yml, on a temporary step that was removed again. So no
|
|
# value pointing at the registry - not even a path segment - lives in the
|
|
# calling repository, and a second owner on the same instance needs no
|
|
# second setting. Two guards make the switch from the old shape loud
|
|
# instead of puzzling: charts-url is rejected unless it ends in
|
|
# "/api/packages" (an old full URL still carrying "/helm", "/api/charts"
|
|
# or the owner is named explicitly), and the owner is rejected when empty
|
|
# or when it holds characters that are not a path segment - an empty one
|
|
# would build "<base>//helm/api/charts", i.e. another bare 404 against a
|
|
# masked URL.
|
|
#
|
|
# Three further 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 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 "<chart>-*.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 "<name>-<version>.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 }}
|
|
REGISTRY_OWNER: ${{ inputs.registry-owner }}
|
|
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}"
|
|
: "${REGISTRY_OWNER:?empty - the registry-owner input did not arrive; the caller passes it from github.repository_owner, do not hardcode an owner}"
|
|
: "${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
|
|
# "<host>:<port>", which that mask would not cover. Mask the host too.
|
|
CHARTS_HOST="${REGISTRY_CHARTS_URL#*://}"
|
|
echo "::add-mask::${CHARTS_HOST%%/*}"
|
|
# charts-url is the base only; this workflow appends the owner, the
|
|
# package type and the endpoint. Reject the old, full shape by name so
|
|
# the failure says what to change in the vault item instead of 404ing
|
|
# against a URL the log has masked.
|
|
CHARTS_BASE="${REGISTRY_CHARTS_URL%/}"
|
|
case "$CHARTS_BASE" in
|
|
*/api/charts)
|
|
echo "::error::op://GitHub-Actions/jlab-registry-access/charts-url still ends in /api/charts - it must hold only the base up to /api/packages (https://<host>/api/packages); this workflow appends /<owner>/helm/api/charts itself"
|
|
exit 1
|
|
;;
|
|
*/helm|*/helm/*)
|
|
echo "::error::op://GitHub-Actions/jlab-registry-access/charts-url still contains the /helm path segment - it must hold only the base up to /api/packages (https://<host>/api/packages); this workflow appends /<owner>/helm/api/charts itself"
|
|
exit 1
|
|
;;
|
|
*/api/packages) ;;
|
|
*)
|
|
echo "::error::op://GitHub-Actions/jlab-registry-access/charts-url must end in /api/packages (https://<host>/api/packages) - no owner, no package type, no endpoint; this workflow appends /<owner>/helm/api/charts itself"
|
|
exit 1
|
|
;;
|
|
esac
|
|
# An empty owner would build "<base>//helm/api/charts", a stray "/" or
|
|
# "?" would build something else entirely - both are bare 404s.
|
|
case "$REGISTRY_OWNER" in
|
|
*[!A-Za-z0-9._-]*)
|
|
echo "::error::the registry-owner input is not usable as a URL path segment: '${REGISTRY_OWNER}'"
|
|
exit 1
|
|
;;
|
|
esac
|
|
CHARTS_API_URL="${CHARTS_BASE}/${REGISTRY_OWNER}/helm/api/charts"
|
|
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" "$CHARTS_API_URL" |
|
|
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 }}
|