# Publish a date-based release every time master moves — and keep HACS able to # see every one of them (t42). # # Versioning is date-based: vYYYY.MM.DD, plus a same-day suffix (YYYY.MM.DD.N) # for further releases in the same UTC day. The decision lives in # scripts/determine-release-version.sh so it can be tested locally: # the manifest `version` is untagged -> that version (bump it to force one) # else today's UTC date is untagged -> vYYYY.MM.DD # else -> vYYYY.MM.DD.N, lowest untagged N >= 2 # # Why the write-back below exists: the resolved version is written into # custom_components/nvidia_shield/manifest.json and committed BEFORE the tag, so # the released artifact and its tag always agree. Without it HACS compares the # installed version against a manifest that never moves — v2026.10.09 was # published while master's manifest still said 2026.10.08, so the second release # of that day was offered to nobody. # # The Gitea release carries a nvidia_shield.zip asset (custom_components/ # nvidia_shield/) so the integration is installable manually from any release. # # No hand-made secrets are required: Gitea mints a token for every job and # exposes it as secrets.GITEA_TOKEN. name: Release on: push: branches: [master] # Manual runs, for the first release (no tags yet) or to re-publish after a # partial failure. workflow_dispatch: # One release at a time. Two pushes in quick succession would otherwise race to # tag the same commit; a later run dedupes safely instead of interleaving. concurrency: group: release-master cancel-in-progress: false permissions: # The job token pushes the manifest commit and the tag, and creates the release. contents: write env: ZIP_NAME: nvidia_shield.zip MANIFEST: custom_components/nvidia_shield/manifest.json jobs: release: runs-on: ubuntu-latest # t42: this job pushes the write-back commit itself, and a run starting from # that commit would release the same version twice. Two layers guard it: the # commit message carries the conventional `[skip ci]` marker, and this # condition re-checks the pushed message, so the guard still holds if a Gitea # version stops honouring the marker. workflow_dispatch has no head_commit, # hence the first clause. if: >- github.event_name == 'workflow_dispatch' || !contains(github.event.head_commit.message || '', '[skip ci]') steps: - uses: actions/checkout@v4 with: # Tags drive versioning decisions, so the clone must carry them. fetch-depth: 0 ref: master token: ${{ secrets.GITEA_TOKEN }} - name: Determine the release version id: version run: | set -euo pipefail # VERSION/TAG/SOURCE/BUMP/RELEASE go to stdout and, because GITHUB_OUTPUT # is set inside a step, to this job's outputs too. RELEASE=no means the # version already exists: tag and publish nothing. scripts/determine-release-version.sh - name: Write the version back into the manifest id: bump if: steps.version.outputs.RELEASE == 'yes' && steps.version.outputs.BUMP == 'yes' env: VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail git config user.name "gitea-actions" git config user.email "actions@notexpectedyet.com" python3 - "$MANIFEST" "$VERSION" <<'PY' import json, pathlib, sys path, version = pathlib.Path(sys.argv[1]), sys.argv[2] data = json.loads(path.read_text(encoding="utf-8")) data["version"] = version path.write_text(json.dumps(data, indent=2) + "\n", encoding="utf-8") PY git add "$MANIFEST" git commit -m "chore(release): v${VERSION} [skip ci]" # Never tag a commit whose manifest the default branch does not carry: # a branch protection rule (or a push that landed underneath this run) # rejecting this push makes the release impossible, so stop loudly and # say what to do instead of publishing an inconsistent artifact. if ! git push origin HEAD:master; then echo "::error::could not push the release commit for v${VERSION}: the" \ "default branch rejected it (branch protection, or it moved during" \ "this run). Nothing was tagged or published. Bump ${MANIFEST} to" \ "${VERSION} manually, commit it, and re-run this workflow." >&2 exit 1 fi - name: Verify the released manifest on master if: steps.version.outputs.RELEASE == 'yes' env: VERSION: ${{ steps.version.outputs.VERSION }} run: | set -euo pipefail # Read the manifest from the REMOTE default branch, not from the # workspace: that is the copy HACS fetches, and it has to already carry # the version about to be tagged. git fetch --quiet origin master REMOTE_VERSION="$(git show "origin/master:${MANIFEST}" | jq -r '.version')" if [ "$REMOTE_VERSION" != "$VERSION" ]; then echo "::error::master's manifest says ${REMOTE_VERSION} but v${VERSION}" \ "is being released — refusing to publish an inconsistent artifact." \ "Re-run once master settles." >&2 exit 1 fi echo "master's manifest is ${REMOTE_VERSION}, matching v${VERSION}" - name: Create tag if: steps.version.outputs.RELEASE == 'yes' env: TAG: ${{ steps.version.outputs.TAG }} run: | set -euo pipefail git config user.name "gitea-actions" git config user.email "actions@notexpectedyet.com" # HEAD is the commit the manifest was just verified on: the bumped one # when a write-back was needed, else the pushed commit itself. The tag # and the artifact therefore name the same version. git tag -a "${TAG}" -m "Release ${TAG}" git push origin "refs/tags/${TAG}" - name: Build the release zip if: steps.version.outputs.RELEASE == 'yes' run: | set -euo pipefail cd custom_components zip -r "../${ZIP_NAME}" nvidia_shield - name: Publish the release if: steps.version.outputs.RELEASE == 'yes' env: VERSION: ${{ steps.version.outputs.VERSION }} TAG: ${{ steps.version.outputs.TAG }} SERVER: ${{ github.server_url }} REPO: ${{ github.repository }} TOKEN: ${{ secrets.GITEA_TOKEN }} run: | set -euo pipefail # Notes: subject lines of the commits in the released range. PREV="$(git describe --tags --abbrev=0 "${TAG}^" 2>/dev/null || true)" if [ -n "$PREV" ]; then RANGE="${PREV}..${TAG}" else RANGE="${TAG}" fi { echo "Release ${TAG}" echo git log --pretty=format:'- %s' "$RANGE" } > /tmp/notes.md # The body is built by an encoder rather than by string interpolation: # release notes are multi-line markdown and quoting them into JSON by # hand is a reliable way to publish a malformed release. python3 - <<'PY' > /tmp/release.json import json, os with open('/tmp/notes.md', encoding='utf-8') as handle: body = handle.read() print(json.dumps({ 'tag_name': 'v' + os.environ['VERSION'], 'name': 'v' + os.environ['VERSION'], 'body': body, 'draft': False, 'prerelease': False, })) PY status="$(curl -sS -o /tmp/release-response.json -w '%{http_code}' \ -X POST "${SERVER}/api/v1/repos/${REPO}/releases" \ -H "Authorization: token ${TOKEN}" \ -H 'Content-Type: application/json' \ --data-binary @/tmp/release.json)" RELEASE_ID="$(jq -r '.id // empty' /tmp/release-response.json)" # 409 is "a release for this tag already exists": resolve the existing # release instead of failing, so the zip still gets attached. if [ -z "$RELEASE_ID" ]; then RELEASE_ID="$(curl -sS "${SERVER}/api/v1/repos/${REPO}/releases/tags/${TAG}" \ -H "Authorization: token ${TOKEN}" | jq -r '.id')" else if [ "$status" != "201" ] && [ "$status" != "200" ]; then echo "release creation failed (HTTP ${status})" >&2 cat /tmp/release-response.json >&2 exit 1 fi fi case "$RELEASE_ID" in ''|*[!0-9]*) echo "could not resolve a release id for ${TAG} (HTTP ${status})" >&2 cat /tmp/release-response.json >&2 exit 1 ;; esac echo "release id: ${RELEASE_ID}" curl -sS -o /tmp/asset-response.json \ -X POST "${SERVER}/api/v1/repos/${REPO}/releases/${RELEASE_ID}/assets?name=${ZIP_NAME}" \ -H "Authorization: token ${TOKEN}" \ -F "attachment=@${ZIP_NAME}" echo "published ${TAG} with ${ZIP_NAME}"