# ==================================================================================================================== # # Authors: # # Patrick Lehmann # # # # ==================================================================================================================== # # Copyright 2020-2025 The pyTooling Authors # # # # Licensed under the Apache License, Version 2.0 (the "License"); # # you may not use this file except in compliance with the License. # # You may obtain a copy of the License at # # # # http://www.apache.org/licenses/LICENSE-2.0 # # # # Unless required by applicable law or agreed to in writing, software # # distributed under the License is distributed on an "AS IS" BASIS, # # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # # See the License for the specific language governing permissions and # # limitations under the License. # # # # SPDX-License-Identifier: Apache-2.0 # # ==================================================================================================================== # name: Nightly on: workflow_call: inputs: ubuntu_image: description: 'Name of the Ubuntu image.' required: false default: 'ubuntu-24.04' type: string nightly_name: description: 'Name of the nightly release.' required: false default: 'nightly' type: string nightly_title: description: 'Title of the nightly release.' required: false default: '' type: string nightly_description: description: 'Description of the nightly release.' required: false default: 'Release of artifacts from latest CI pipeline.' type: string draft: description: 'Specify if this is a draft.' required: false default: false type: boolean prerelease: description: 'Specify if this is a pre-release.' required: false default: false type: boolean latest: description: 'Specify if this is the latest release.' required: false default: false type: boolean replacements: description: 'Multi-line string containing search=replace patterns.' required: false default: '' type: string assets: description: 'Multi-line string containing artifact:file:title asset descriptions.' required: true type: string inventory-json: type: string required: false default: '' inventory-version: type: string required: false default: '' inventory-categories: type: string required: false default: '' tarball-name: type: string required: false default: '__pyTooling_upload_artifact__.tar' can-fail: type: boolean required: false default: false jobs: Release: name: 📝 Update 'Nightly Page' on GitHub runs-on: ${{ inputs.ubuntu_image }} continue-on-error: ${{ inputs.can-fail }} permissions: contents: write actions: write # attestations: write steps: - name: ⚠️ Deprecation Warning run: printf "::warning title=%s::%s\n" "NightlyRelease" "'NightlyRelease.yml' template is deprecated. Please switch to 'PublishReleaseNotes.yml'. See https://pytooling.github.io/Actions/JobTemplate/Release/PublishReleaseNotes.html" - name: ⏬ Checkout repository uses: actions/checkout@v5 with: # The command 'git describe' (used for version) needs the history. fetch-depth: 0 - name: 🔧 Install zstd run: sudo apt-get install -y --no-install-recommends zstd - name: 📑 Delete (old) Release Page id: deleteReleasePage run: | set +e ANSI_LIGHT_RED=$'\x1b[91m' ANSI_LIGHT_GREEN=$'\x1b[92m' ANSI_LIGHT_YELLOW=$'\x1b[93m' ANSI_NOCOLOR=$'\x1b[0m' export GH_TOKEN=${{ github.token }} printf "%s" "Deleting release '${{ inputs.nightly_name }}' ... " message="$(gh release delete ${{ inputs.nightly_name }} --yes 2>&1)" if [[ $? -eq 0 ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" elif [[ "${message}" == "release not found" ]]; then printf "%s\n" "${ANSI_LIGHT_YELLOW}[NOT FOUND]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't delete release '${{ inputs.nightly_name }}' -> Error: '${message}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "InternalError" "Couldn't delete release '${{ inputs.nightly_name }}' -> Error: '${message}'." exit 1 fi - name: 📑 (Re)create (new) Release Page id: createReleasePage run: | set +e ANSI_LIGHT_RED=$'\x1b[91m' ANSI_LIGHT_GREEN=$'\x1b[92m' ANSI_NOCOLOR=$'\x1b[0m' export GH_TOKEN=${{ github.token }} addDraft="--draft" if [[ "${{ inputs.prerelease }}" == "true" ]]; then addPreRelease="--prerelease" fi if [[ "${{ inputs.latest }}" == "false" ]]; then addLatest="--latest=false" fi if [[ "${{ inputs.nightly_title }}" != "" ]]; then addTitle=("--title" "${{ inputs.nightly_title }}") fi cat <<'EOF' > __NoTeS__.md ${{ inputs.nightly_description }} EOF if [[ -s __NoTeS__.md ]]; then addNotes=("--notes-file" "__NoTeS__.md") fi # Apply replacements while IFS=$'\r\n' read -r patternLine; do # skip empty lines [[ "$patternLine" == "" ]] && continue pattern="${patternLine%%=*}" replacement="${patternLine#*=}" sed -i -e "s/%$pattern%/$replacement/g" "__NoTeS__.md" done <<<'${{ inputs.replacements }}' # Add footer line cat <> __NoTeS__.md -------- Published from [${{ github.workflow }}](https://github.com/Paebbels/ghdl/actions/runs/${{ github.run_id }}) workflow triggered by @${{ github.actor }} on $(date '+%Y-%m-%d %H:%M:%S %Z'). EOF printf "%s\n" "Creating release '${{ inputs.nightly_name }}' ... " message="$(gh release create "${{ inputs.nightly_name }}" --verify-tag $addDraft $addPreRelease $addLatest "${addTitle[@]}" "${addNotes[@]}" 2>&1)" if [[ $? -eq 0 ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't create release '${{ inputs.nightly_name }}' -> Error: '${message}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "InternalError" "Couldn't create release '${{ inputs.nightly_name }}' -> Error: '${message}'." exit 1 fi - name: 📥 Download artifacts and upload as assets id: uploadAssets run: | set +e ANSI_LIGHT_RED=$'\x1b[91m' ANSI_LIGHT_GREEN=$'\x1b[92m' ANSI_LIGHT_YELLOW=$'\x1b[93m' ANSI_LIGHT_BLUE=$'\x1b[94m' ANSI_NOCOLOR=$'\x1b[0m' export GH_TOKEN=${{ github.token }} Replace() { line="$1" while IFS=$'\r\n' read -r patternLine; do # skip empty lines [[ "$patternLine" == "" ]] && continue pattern="${patternLine%%=*}" replacement="${patternLine#*=}" line="${line//"%$pattern%"/"$replacement"}" done <<<'${{ inputs.replacements }}' printf "%s\n" "$line" } # Create JSON inventory if [[ "${{ inputs.inventory-json }}" != "" ]]; then VERSION="1.0" # Split categories by ',' into a Bash array. # See https://stackoverflow.com/a/45201229/3719459 if [[ "${{ inputs.inventory-categories }}" != "" ]]; then readarray -td, inventoryCategories <<<"${{ inputs.inventory-categories }}," unset 'inventoryCategories[-1]' declare -p inventoryCategories else inventoryCategories="" fi jsonInventory=$(jq -c -n \ --arg version "${VERSION}" \ --arg date "$(date +"%Y-%m-%dT%H-%M-%S%:z")" \ --argjson jsonMeta "$(jq -c -n \ --arg tag "${{ inputs.nightly_name }}" \ --arg version "${{ inputs.inventory-version }}" \ --arg hash "${{ github.sha }}" \ --arg repo "${{ github.server_url }}/${{ github.repository }}" \ --arg release "${{ github.server_url }}/${{ github.repository }}/releases/download/${{ inputs.nightly_name }}" \ --argjson categories "$(jq -c -n \ '$ARGS.positional' \ --args "${inventoryCategories[@]}" \ )" \ '{"tag": $tag, "version": $version, "git-hash": $hash, "repository-url": $repo, "release-url": $release, "categories": $categories}' \ )" \ '{"version": 1.0, "timestamp": $date, "meta": $jsonMeta, "files": {}}' ) fi ERRORS=0 # A dictionary of 0/1 to avoid duplicate downloads declare -A downloadedArtifacts # A dictionary to check for duplicate asset files in release declare -A assetFilenames while IFS=$'\r\n' read -r assetLine; do if [[ "${assetLine}" == "" || "${assetLine:0:1}" == "#" ]]; then continue fi # split assetLine colon separated triple: artifact:asset:title artifact="${assetLine%%:*}" assetLine="${assetLine#*:}" asset="${assetLine%%:*}" assetLine="${assetLine#*:}" if [[ "${{ inputs.inventory-json }}" == "" ]]; then categories="" title="${assetLine##*:}" else categories="${assetLine%%:*}" title="${assetLine##*:}" fi # remove leading whitespace asset="${asset#"${asset%%[![:space:]]*}"}" categories="${categories#"${categories%%[![:space:]]*}"}" title="${title#"${title%%[![:space:]]*}"}" # apply replacements asset="$(Replace "${asset}")" title="$(Replace "${title}")" printf "%s\n" "Publish asset '${asset}' from artifact '${artifact}' with title '${title}'" printf " %s" "Checked asset for duplicates ... " if [[ -n "${assetFilenames[$asset]}" ]]; then printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "DuplicateAsset" "Asset '${asset}' from artifact '${artifact}' was already uploaded to release '${{ inputs.nightly_name }}'." ERRORS=$((ERRORS + 1)) continue else printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" assetFilenames[$asset]=1 fi # Download artifact by artifact name if [[ -n "${downloadedArtifacts[$artifact]}" ]]; then printf " %s\n" "downloading '${artifact}' ... ${ANSI_LIGHT_YELLOW}[SKIPPED]${ANSI_NOCOLOR}" else printf " downloading '${artifact}' ...\n" printf " %s" "gh run download $GITHUB_RUN_ID --dir \"${artifact}\" --name \"${artifact}\" " gh run download $GITHUB_RUN_ID --dir "${artifact}" --name "${artifact}" if [[ $? -eq 0 ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't download artifact '${artifact}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "ArtifactNotFound" "Couldn't download artifact '${artifact}'." ERRORS=$((ERRORS + 1)) continue fi downloadedArtifacts[$artifact]=1 printf " %s" "Checking for embedded tarball ... " if [[ -f "${artifact}/${{ inputs.tarball-name }}" ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[FOUND]${ANSI_NOCOLOR}" pushd "${artifact}" > /dev/null printf " %s" "Extracting embedded tarball ... " tar -xf "${{ inputs.tarball-name }}" if [[ $? -ne 0 ]]; then printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" fi printf " %s" "Removing temporary tarball ... " rm -f "${{ inputs.tarball-name }}" if [[ $? -ne 0 ]]; then printf "%s\n" "${ANSI_LIGHT_RED}[FAILED]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" fi popd > /dev/null else printf "%s\n" "${ANSI_LIGHT_YELLOW}[SKIPPED]${ANSI_NOCOLOR}" fi fi # Check if artifact should be compressed (zip, tgz) or if asset was part of the downloaded artifact. printf " %s" "checking asset '${artifact}/${asset}' ... " if [[ "${asset}" == !*.zip ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[ZIP]${ANSI_NOCOLOR}" asset="${asset##*!}" printf "::group:: %s\n" "Compressing artifact '${artifact}' to '${asset}' ..." ( cd "${artifact}" && \ zip -r "../${asset}" * ) retCode=$? printf "::endgroup::\n" if [[ $retCode -eq 0 ]]; then printf " %s\n" "Compression ${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" uploadFile="${asset}" else printf " %s\n" "Compression ${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't compress '${artifact}' to zip file '${asset}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "CompressionError" "Couldn't compress '${artifact}' to zip file '${asset}'." ERRORS=$((ERRORS + 1)) continue fi elif [[ "${asset}" == !*.tgz || "${asset}" == !*.tar.gz || "${asset}" == \$*.tgz || "${asset}" == \$*.tar.gz ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[TAR/GZ]${ANSI_NOCOLOR}" if [[ "${asset:0:1}" == "\$" ]]; then asset="${asset##*$}" dirName="${asset%.*}" printf " %s\n" "Compressing artifact '${artifact}' to '${asset}' ..." tar -c --gzip --owner=0 --group=0 --file="${asset}" --directory="${artifact}" --transform "s|^\.|${dirName%.tar}|" . retCode=$? else asset="${asset##*!}" printf " %s\n" "Compressing artifact '${artifact}' to '${asset}' ..." ( cd "${artifact}" && \ tar -c --gzip --owner=0 --group=0 --file="../${asset}" * ) retCode=$? fi if [[ $retCode -eq 0 ]]; then printf " %s\n" "Compression ${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" uploadFile="${asset}" else printf " %s\n" "Compression ${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't compress '${artifact}' to tgz file '${asset}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "CompressionError" "Couldn't compress '${artifact}' to tgz file '${asset}'." ERRORS=$((ERRORS + 1)) continue fi elif [[ "${asset}" == !*.tzst || "${asset}" == !*.tar.zst || "${asset}" == \$*.tzst || "${asset}" == \$*.tar.zst ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[ZST]${ANSI_NOCOLOR}" if [[ "${asset:0:1}" == "\$" ]]; then asset="${asset##*$}" dirName="${asset%.*}" printf " %s\n" "Compressing artifact '${artifact}' to '${asset}' ..." tar -c --zstd --owner=0 --group=0 --file="${asset}" --directory="${artifact}" --transform "s|^\.|${dirName%.tar}|" . retCode=$? else asset="${asset##*!}" printf " %s\n" "Compressing artifact '${artifact}' to '${asset}' ..." ( cd "${artifact}" && \ tar -c --zstd --owner=0 --group=0 --file="../${asset}" * ) retCode=$? fi if [[ $retCode -eq 0 ]]; then printf " %s\n" "Compression ${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" uploadFile="${asset}" else printf " %s\n" "Compression ${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't compress '${artifact}' to zst file '${asset}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "CompressionError" "Couldn't compress '${artifact}' to zst file '${asset}'." ERRORS=$((ERRORS + 1)) continue fi elif [[ -e "${artifact}/${asset}" ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" uploadFile="${artifact}/${asset}" else printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't find asset '${asset}' in artifact '${artifact}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "FileNotFound" "Couldn't find asset '${asset}' in artifact '${artifact}'." ERRORS=$((ERRORS + 1)) continue fi # Add asset to JSON inventory if [[ "${{ inputs.inventory-json }}" != "" ]]; then if [[ "${categories}" != "${title}" ]]; then printf " %s\n" "adding file '${uploadFile#*/}' with '${categories//;/ → }' to JSON inventory ..." category="" jsonEntry=$(jq -c -n \ --arg title "${title}" \ --arg file "${uploadFile#*/}" \ '{"file": $file, "title": $title}' \ ) while [[ "${categories}" != "${category}" ]]; do category="${categories##*,}" categories="${categories%,*}" jsonEntry=$(jq -c -n --arg cat "${category}" --argjson value "${jsonEntry}" '{$cat: $value}') done jsonInventory=$(jq -c -n \ --argjson inventory "${jsonInventory}" \ --argjson file "${jsonEntry}" \ '$inventory * {"files": $file}' \ ) else printf " %s\n" "adding file '${uploadFile#*/}' to JSON inventory ... ${ANSI_LIGHT_YELLOW}[SKIPPED]${ANSI_NOCOLOR}" fi fi # Upload asset to existing release page printf " %s" "uploading asset '${asset}' from '${uploadFile}' with title '${title}' ... " gh release upload ${{ inputs.nightly_name }} "${uploadFile}#${title}" --clobber if [[ $? -eq 0 ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't upload asset '${asset}' from '${uploadFile}' to release '${{ inputs.nightly_name }}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "UploadError" "Couldn't upload asset '${asset}' from '${uploadFile}' to release '${{ inputs.nightly_name }}'." ERRORS=$((ERRORS + 1)) continue fi done <<<'${{ inputs.assets }}' if [[ "${{ inputs.inventory-json }}" != "" ]]; then inventoryTitle="Release Inventory (JSON)" printf "%s\n" "Publish asset '${{ inputs.inventory-json }}' with title '${inventoryTitle}'" printf "::group::${ANSI_LIGHT_BLUE}%s${ANSI_NOCOLOR}\n" "Writing JSON inventory to '${{ inputs.inventory-json }}' ...." printf "%s\n" "$(jq -n --argjson inventory "${jsonInventory}" '$inventory')" > "${{ inputs.inventory-json }}" cat "${{ inputs.inventory-json }}" printf "::endgroup::\n" # Upload inventory asset to existing release page printf " %s" "uploading asset '${{ inputs.inventory-json }}' title '${inventoryTitle}' ... " gh release upload ${{ inputs.nightly_name }} "${{ inputs.inventory-json }}#${inventoryTitle}" --clobber if [[ $? -eq 0 ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't upload asset '${{ inputs.inventory-json }}' to release '${{ inputs.nightly_name }}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "UploadError" "Couldn't upload asset '${{ inputs.inventory-json }}' to release '${{ inputs.nightly_name }}'." ERRORS=$((ERRORS + 1)) continue fi fi printf "::group::${ANSI_LIGHT_BLUE}%s${ANSI_NOCOLOR}\n" "Inspecting downloaded artifacts ..." tree -pash -L 3 . printf "::endgroup::\n" if [[ $ERRORS -ne 0 ]]; then printf "%s\n" "${ANSI_LIGHT_RED}${ERRORS} errors detected in previous steps.${ANSI_NOCOLOR}" exit 1 fi - name: 📑 Remove draft state from Release Page if: ${{ ! inputs.draft }} run: | set +e ANSI_LIGHT_RED=$'\x1b[91m' ANSI_LIGHT_GREEN=$'\x1b[92m' ANSI_NOCOLOR=$'\x1b[0m' export GH_TOKEN=${{ github.token }} # Remove draft-state from release page printf "%s" "Remove draft-state from release '${title}' ... " gh release edit --draft=false "${{ inputs.nightly_name }}" if [[ $? -eq 0 ]]; then printf "%s\n" "${ANSI_LIGHT_GREEN}[OK]${ANSI_NOCOLOR}" else printf "%s\n" "${ANSI_LIGHT_RED}[ERROR]${ANSI_NOCOLOR}" printf " %s\n" "${ANSI_LIGHT_RED}Couldn't remove draft-state from release '${{ inputs.nightly_name }}'.${ANSI_NOCOLOR}" printf "::error title=%s::%s\n" "ReleasePage" "Couldn't remove draft-state from release '${{ inputs.nightly_name }}'." fi