From ea644fd33fcd71d0ec973894eeb27f14dc4795fe Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Mon, 19 Oct 2026 11:48:38 +0200 Subject: [PATCH 1/2] ci(release): infer optional workflow tags Signed-off-by: Simon Scatton --- .agents/skills/release-openshell/SKILL.md | 37 ++++++++- .agents/skills/watch-github-actions/SKILL.md | 7 ++ .github/workflows/release-auto-tag.yml | 60 ++++---------- .github/workflows/release-build.yml | 4 +- .github/workflows/release-prerelease.yml | 82 +++++++++++++++++--- .github/workflows/release.yml | 6 +- CI.md | 36 ++++++++- tasks/scripts/next-prerelease-tag.py | 76 ++++++++++++++++++ tasks/scripts/select-release-candidate.py | 20 ++++- 9 files changed, 265 insertions(+), 63 deletions(-) create mode 100644 tasks/scripts/next-prerelease-tag.py diff --git a/.agents/skills/release-openshell/SKILL.md b/.agents/skills/release-openshell/SKILL.md index bdd81e725a..f5f382e4ea 100644 --- a/.agents/skills/release-openshell/SKILL.md +++ b/.agents/skills/release-openshell/SKILL.md @@ -12,11 +12,17 @@ prerelease and changes since the previous stable release. Present the candidate to the maintainer. Once approved, dispatch Release from that branch: ```shell -gh workflow run release.yml --ref main -f tag=vX.Y.Z +gh workflow run release.yml --ref main ``` -Use the support branch instead of `main` for a previous minor version. Release -selects the highest-numbered `vX.Y.Z-pre.N` reachable from that branch with a +Without `tag`, Release detects the highest prerelease version reachable from the +selected branch, then selects its highest-numbered passing candidate. It stops +if that version has no qualified candidate or already has a stable tag. Supply +`-f tag=vX.Y.Z` to select a version explicitly or retry a stable release. + +Use the support branch instead of `main` for a previous minor version. That +branch must contain the updated release workflows. Release selects the +highest-numbered `vX.Y.Z-pre.N` reachable from that branch with a successful Pre-release run triggered by that tag or dispatched on the same branch, and matching passing `release-tag-v1` qualification evidence. Missing or expired evidence blocks promotion. Inspect the selected candidate in the promotion job summary. @@ -39,6 +45,31 @@ created using `GITHUB_TOKEN`, dispatch Pre-release explicitly on the intended br gh workflow run release-prerelease.yml --ref main -f tag=vX.Y.Z-pre.1 ``` +Once the first tag seeds a series, omit `tag` to create the next candidate at +the selected branch's dispatch commit: + +```shell +gh workflow run release-prerelease.yml --ref main +``` + +This selects the highest release series reachable from that branch and requires +its `-pre.1` seed there. It stops if that version is already stable. New version +series always need an explicit seed. Manual runs create a new candidate even +without new commits; pass an existing tag explicitly to retry that candidate. +Candidate creation is serialized with Release Auto-Tag, which still skips +unchanged commits and inactive series on `main`. + +For example, start a support patch series by tagging its intended branch commit +as `v0.4.4-pre.1`. Subsequent candidates and promotion use the support branch: + +```shell +gh workflow run release-prerelease.yml --ref release/0.4 +gh workflow run release.yml --ref release/0.4 +``` + +Replace `release/0.4` with the actual support branch. Use explicit tag inputs if +its history also contains a higher version series. + Wait for the entire Pre-release run to pass before promoting it. Stable tag pushes do not start Release. Release Auto-Tag creates and dispatches prereleases on `main`. diff --git a/.agents/skills/watch-github-actions/SKILL.md b/.agents/skills/watch-github-actions/SKILL.md index 39868baff1..8d618e6d20 100644 --- a/.agents/skills/watch-github-actions/SKILL.md +++ b/.agents/skills/watch-github-actions/SKILL.md @@ -176,6 +176,13 @@ selects the latest passing prerelease for the requested version and branch, verifies its qualification evidence, creates the stable tag at its exact commit, then rebuilds and publishes without rerunning qualification. The promotion job summary and `stable-promotion-*` artifact record the candidate and evidence. +Both workflows accept optional tags. Tagless Pre-release creates the next +candidate in the selected branch's seeded series; inspect Resolve Prerelease Tag +for the actual tag and commit. Tagless Release infers the highest reachable +prerelease version and stops if it is already stable. Explicit tags select an +existing candidate or stable version for retries. Release Auto-Tag and manual +candidate creation share a concurrency group; pending tag-resolution jobs can +be waiting for that lock. Failed, cancelled, or skipped required suites fail qualification; stable publication is blocked, while published pre-release artifacts remain available. The Pre-release failure notification includes the diff --git a/.github/workflows/release-auto-tag.yml b/.github/workflows/release-auto-tag.yml index 2c5892aa4e..60b8bd3c5d 100644 --- a/.github/workflows/release-auto-tag.yml +++ b/.github/workflows/release-auto-tag.yml @@ -21,69 +21,43 @@ jobs: create-tag: runs-on: ubuntu-latest timeout-minutes: 5 + concurrency: + group: release-prerelease-tags + cancel-in-progress: false + queue: max steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: ref: main fetch-depth: 0 + persist-credentials: false + + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.10.12" + python-version: "3.13" + enable-cache: false - name: Determine next pre-release version id: version run: | set -euo pipefail - tags=$(git tag -l) - latest=$(printf '%s\n' "$tags" | \ - { grep -E '^v[0-9]+\.[0-9]+\.[0-9]+(-pre\.[1-9][0-9]*)?$' || true; } | \ - sort -V | tail -1) - if [[ ! "$latest" =~ ^(v[0-9]+\.[0-9]+\.[0-9]+)-pre\.([1-9][0-9]*)$ ]]; then - echo "No active pre-release series — skipping tag creation" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - base=${BASH_REMATCH[1]} - number=${BASH_REMATCH[2]} - if ! git show-ref --verify --quiet "refs/tags/${base}-pre.1"; then - echo "No ${base}-pre.1 seed — skipping tag creation" + next=$(uv run --no-project python tasks/scripts/next-prerelease-tag.py --scheduled) + if [[ -z "${next}" ]]; then echo "skip=true" >> "$GITHUB_OUTPUT" exit 0 fi - if git show-ref --verify --quiet "refs/tags/${base}"; then - echo "$base is already stable — skipping tag creation" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - if ! git merge-base --is-ancestor "$latest" HEAD; then - echo "::error::Latest pre-release $latest is not an ancestor of main" - exit 1 - fi - echo "Latest tag: $latest" - - # Skip if no new commits since the latest tag - commit_count=$(git rev-list "${latest}..HEAD" --count) - echo "Commits since $latest: $commit_count" - if [ "$commit_count" -eq 0 ]; then - echo "No new commits since $latest — skipping tag creation" - echo "skip=true" >> "$GITHUB_OUTPUT" - exit 0 - fi - - next="${base}-pre.$((number + 1))" - - if git tag -l "$next" | grep -q .; then - echo "::error::Tag $next already exists" - exit 1 - fi - echo "next=$next" >> "$GITHUB_OUTPUT" echo "Next tag: $next" - - name: Create and push tag + - name: Create tag if: steps.version.outputs.skip != 'true' env: + GH_TOKEN: ${{ github.token }} NEXT_TAG: ${{ steps.version.outputs.next }} run: | - git tag "$NEXT_TAG" - git push origin "refs/tags/$NEXT_TAG" + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f "ref=refs/tags/${NEXT_TAG}" -f "sha=$(git rev-parse HEAD)" - name: Trigger Pre-release workflow if: steps.version.outputs.skip != 'true' diff --git a/.github/workflows/release-build.yml b/.github/workflows/release-build.yml index e92a05df87..c85d29d2fc 100644 --- a/.github/workflows/release-build.yml +++ b/.github/workflows/release-build.yml @@ -11,7 +11,7 @@ on: required: true type: string expected-source-sha: - description: Qualified source commit that the release tag must resolve to + description: Expected source commit that the release tag must resolve to required: false default: "" type: string @@ -89,7 +89,7 @@ jobs: run: | set -euo pipefail if [[ -n "${EXPECTED_SOURCE_SHA}" && "$(git rev-parse HEAD)" != "${EXPECTED_SOURCE_SHA}" ]]; then - echo "Release tag ${RELEASE_TAG} does not match the qualified commit ${EXPECTED_SOURCE_SHA}." >&2 + echo "Release tag ${RELEASE_TAG} does not match the expected commit ${EXPECTED_SOURCE_SHA}." >&2 exit 1 fi if [[ "${RELEASE_TAG}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then diff --git a/.github/workflows/release-prerelease.yml b/.github/workflows/release-prerelease.yml index 2c2e8d8c86..b67d8b3d6b 100644 --- a/.github/workflows/release-prerelease.yml +++ b/.github/workflows/release-prerelease.yml @@ -10,23 +10,87 @@ on: workflow_dispatch: inputs: tag: - description: "Pre-release tag to publish and qualify (e.g. v0.0.4-pre.1)" - required: true + description: "Existing prerelease tag (e.g. v0.0.4-pre.1); omit to create the next candidate on this branch" + required: false type: string permissions: contents: read jobs: + resolve: + name: Resolve Prerelease Tag + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: + contents: write + concurrency: + group: release-prerelease-tags + cancel-in-progress: false + queue: max + outputs: + tag: ${{ steps.tag.outputs.tag }} + source-sha: ${{ steps.tag.outputs.source_sha }} + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + fetch-depth: 0 + persist-credentials: false + + - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1 + with: + version: "0.10.12" + python-version: "3.13" + enable-cache: false + + - name: Resolve or create candidate tag + id: tag + env: + GH_TOKEN: ${{ github.token }} + RELEASE_TAG: ${{ inputs.tag }} + EVENT_NAME: ${{ github.event_name }} + REF_TYPE: ${{ github.ref_type }} + REF_NAME: ${{ github.ref_name }} + run: | + set -euo pipefail + tag="${RELEASE_TAG}" + if [[ "${EVENT_NAME}" == "push" ]]; then + tag="${REF_NAME}" + fi + if [[ -z "${tag}" ]]; then + test "${REF_TYPE}" = branch + tag=$(uv run --no-project python tasks/scripts/next-prerelease-tag.py) + source_sha=$(git rev-parse HEAD) + gh api --method POST "repos/${GITHUB_REPOSITORY}/git/refs" \ + -f "ref=refs/tags/${tag}" -f "sha=${source_sha}" + else + if [[ ! "${tag}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+-pre\.[1-9][0-9]*$ ]]; then + echo "Unsupported prerelease tag: ${tag}" >&2 + exit 1 + fi + source_sha=$(git rev-parse "refs/tags/${tag}^{commit}") + fi + { + echo "tag=${tag}" + echo "source_sha=${source_sha}" + } >> "${GITHUB_OUTPUT}" + { + echo "## Prerelease candidate" + echo "- Tag: \`${tag}\`" + echo "- Source commit: \`${source_sha}\`" + } >> "${GITHUB_STEP_SUMMARY}" + build: name: Build + needs: resolve permissions: actions: read contents: read packages: write uses: ./.github/workflows/release-build.yml with: - release-tag: ${{ inputs.tag || github.ref_name }} + release-tag: ${{ needs.resolve.outputs.tag }} + expected-source-sha: ${{ needs.resolve.outputs.source-sha }} is-prerelease: true secrets: CACHIX_AUTH_TOKEN: ${{ secrets.CACHIX_AUTH_TOKEN }} @@ -46,7 +110,7 @@ jobs: publish: name: Publish - needs: build + needs: [resolve, build] permissions: actions: read contents: write @@ -56,7 +120,7 @@ jobs: artifact-metadata: write uses: ./.github/workflows/release-publish.yml with: - release-tag: ${{ inputs.tag || github.ref_name }} + release-tag: ${{ needs.resolve.outputs.tag }} source-sha: ${{ needs.build.outputs.source_sha }} semver: ${{ needs.build.outputs.semver }} python-version: ${{ needs.build.outputs.python_version }} @@ -65,7 +129,7 @@ jobs: qualify: name: Qualify - needs: build + needs: [resolve, build] permissions: actions: read contents: read @@ -74,7 +138,7 @@ jobs: pull-requests: read uses: ./.github/workflows/release-qualification.yml with: - release-tag: ${{ inputs.tag || github.ref_name }} + release-tag: ${{ needs.resolve.outputs.tag }} source-sha: ${{ needs.build.outputs.source_sha }} is-prerelease: true secrets: @@ -111,7 +175,7 @@ jobs: notify-prerelease-failure: name: Notify Prerelease Failure - needs: [build, publish, qualify] + needs: [resolve, build, publish, qualify] if: failure() runs-on: ubuntu-latest timeout-minutes: 2 @@ -124,7 +188,7 @@ jobs: - name: Send Slack notification continue-on-error: true env: - RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag || inputs.tag || github.ref_name }} PUBLICATION_RESULT: ${{ needs.publish.result }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }} SLACK_MENTION: ${{ secrets.SLACK_OPENSHELL_TRIAGE_MENTION }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index fe2299829b..2371673224 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -7,8 +7,8 @@ on: workflow_dispatch: inputs: tag: - description: "Stable version to promote from its latest passing prerelease (e.g. v0.0.4)" - required: true + description: "Stable version (e.g. v0.0.4); omit to detect the highest prerelease version on this branch" + required: false type: string notes-only: description: "Generate a release notes artifact without promoting or publishing" @@ -86,7 +86,7 @@ jobs: - name: Retain promotion record uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7 with: - name: stable-promotion-${{ inputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }} + name: stable-promotion-${{ steps.candidate.outputs.tag }}-${{ github.run_id }}-${{ github.run_attempt }} path: promotion.json retention-days: 90 if-no-files-found: error diff --git a/CI.md b/CI.md index 78e98fc016..69e968c5f2 100644 --- a/CI.md +++ b/CI.md @@ -351,7 +351,34 @@ gh workflow run release.yml --ref main \ -f tag=v0.1.4 -f notes-only=true ``` -Dispatch Release from the intended release branch with a stable version: +Both release workflows accept an optional `tag` input. After seeding a version +series with a `vMAJOR.MINOR.PATCH-pre.1` tag on the intended branch, dispatch +Pre-release without a tag to create its next candidate: + +```shell +gh workflow run release-prerelease.yml --ref main +``` + +Pre-release chooses the highest release series reachable from the selected +branch and requires that series' `-pre.1` seed in its history. It stops if the +version is already stable or no series exists. Candidate numbers account for +all tags in the same series across the repository, and tag creation is serialized +with Release Auto-Tag. The new tag points to the branch's dispatch commit; +build, publication, qualification, and notifications use the resolved tag. +An explicit tag continues to build that existing candidate. Manual tagless runs +create a candidate even without new commits, so use an explicit tag for retries. +Release Auto-Tag continues to skip unchanged commits and inactive series on `main`. + +Dispatch Release from the intended release branch: + +```shell +gh workflow run release.yml --ref main +``` + +Without `tag`, Release detects the highest prerelease version reachable from +that branch, then selects a qualified candidate within that version. It stops +if none qualifies or that version is already stable. Supply a stable version +to select it explicitly or retry a stable release: ```shell gh workflow run release.yml --ref main -f tag=v0.4.3 @@ -360,6 +387,11 @@ gh workflow run release.yml --ref main -f tag=v0.4.3 For a support release, replace `main` with its support branch. Release chooses the highest-numbered `v0.4.3-pre.N` reachable from the selected branch with a successful Pre-release run triggered by the tag or dispatched on that branch. +The support branch must contain these workflow definitions. Start each new +support patch series with an explicit seed such as `v0.4.4-pre.1`; subsequent +Pre-release and Release dispatches can omit `tag`. If the branch's history also +contains a higher version series, supply `tag` explicitly to choose the intended +support version. The entire Pre-release run must succeed, including publication. If qualification passes but publication fails, rerun the failed jobs before promoting the candidate. It verifies the qualification summary's @@ -634,7 +666,7 @@ These workflows run after merge to publish dev/tagged artifacts and verify them. |---|---| | `.github/workflows/release-dev.yml` | Publishes the rolling `dev` build on every push to `main`. Builds gateway, sandbox, and supervisor images and binaries, packages, wheels, and pushes the Helm chart as `oci://ghcr.io/nvidia/openshell/helm-chart:0.0.0-dev` (plus an immutable `0.0.0-dev.` pin). Also dispatchable manually. | | `.github/workflows/release.yml` | Manually promote the latest qualified prerelease for a version and branch, create its stable tag, then rebuild and publish. Support releases preserve shared latest channels. The notes-only mode generates a release notes preview artifact. | -| `.github/workflows/release-prerelease.yml` | Pre-release orchestrator, triggered by prerelease tag pushes or dispatched by Release Auto-Tag or manually: build, then publish and qualify in parallel. Qualification failure fails the run and sends the failure notification; published artifacts remain available. | +| `.github/workflows/release-prerelease.yml` | Pre-release orchestrator, triggered by prerelease tag pushes or dispatched by Release Auto-Tag or manually. A tagless dispatch creates the next candidate on its branch; explicit tags select existing candidates. Builds, then publishes and qualifies in parallel. Qualification failure fails the run and sends the failure notification; published artifacts remain available. | | `.github/workflows/release-build.yml` | Shared version computation, binary and image builds, and package builds. Validates that the tag matches the caller's release kind. | | `.github/workflows/release-publish.yml` | Shared artifact assembly, OCI image tags and Helm chart publication, and package publication. Stable releases also publish to GitHub Releases, snap, npm, PyPI, and Fern. | | `.github/workflows/release-notes.yml` | Shared prompt-driven release notes generation through Inference Hub, used by stable publication and notes-only previews. | diff --git a/tasks/scripts/next-prerelease-tag.py b/tasks/scripts/next-prerelease-tag.py new file mode 100644 index 0000000000..e4e5f05402 --- /dev/null +++ b/tasks/scripts/next-prerelease-tag.py @@ -0,0 +1,76 @@ +#!/usr/bin/env python3 + +# SPDX-FileCopyrightText: Copyright (c) 2025-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Choose the next candidate in the checked-out branch's seeded prerelease series.""" + +from __future__ import annotations + +import argparse +import re +import subprocess +import sys + + +def git(*args: str) -> str: + return subprocess.check_output(["git", *args], text=True).strip() + + +def next_tag(*, scheduled: bool = False) -> str: + versions = {} + for tag in git("tag", "--merged", "HEAD", "--list", "v*").splitlines(): + match = re.fullmatch( + r"v([0-9]+)\.([0-9]+)\.([0-9]+)(?:-pre\.([1-9][0-9]*))?", tag + ) + if match: + versions[tag] = ( + *map(int, match.groups()[:3]), + match[4] is None, + int(match[4] or 0), + ) + if not versions: + raise ValueError( + "No release series on this branch; create a vMAJOR.MINOR.PATCH-pre.1 seed" + ) + latest = max(versions, key=versions.__getitem__) + base, separator, _ = latest.partition("-pre.") + if not separator or git("tag", "--list", base): + raise ValueError( + f"{base} is already stable; seed the next prerelease version explicitly" + ) + if f"{base}-pre.1" not in versions: + raise ValueError(f"No {base}-pre.1 seed is reachable from this branch") + if scheduled and git("rev-list", f"{latest}..HEAD", "--count") == "0": + return "" + + # Tag names are repository-wide even when branches share the same series. + numbers = [ + int(tag.rsplit(".", 1)[1]) + for tag in git("tag", "--list", f"{base}-pre.*").splitlines() + if re.fullmatch(re.escape(base) + r"-pre\.[1-9][0-9]*", tag) + ] + return f"{base}-pre.{max(numbers) + 1}" + + +def main() -> None: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument( + "--scheduled", + action="store_true", + help="Skip inactive series and unchanged commits", + ) + args = parser.parse_args() + try: + tag = next_tag(scheduled=args.scheduled) + except ValueError as error: + if not args.scheduled: + parser.error(str(error)) + print(error, file=sys.stderr) + return + if tag: + print(tag) + + +if __name__ == "__main__": + main() diff --git a/tasks/scripts/select-release-candidate.py b/tasks/scripts/select-release-candidate.py index 2030bdbdb5..fadc29d9fc 100644 --- a/tasks/scripts/select-release-candidate.py +++ b/tasks/scripts/select-release-candidate.py @@ -61,6 +61,24 @@ def is_latest(tag: str) -> bool: def select_candidate(tag: str, branch: str) -> dict: + if not tag: + versions = {} + for candidate in git( + "tag", "--merged", "HEAD", "--list", "v*-pre.*" + ).splitlines(): + match = re.fullmatch( + r"(v([0-9]+)\.([0-9]+)\.([0-9]+))-pre\.[1-9][0-9]*", candidate + ) + if match: + versions[match[1]] = tuple(map(int, match.groups()[1:])) + if not versions: + raise ValueError(f"No prerelease versions are reachable from {branch}") + tag = max(versions, key=versions.__getitem__) + if git("tag", "--list", tag): + raise ValueError( + f"{tag} is already stable; supply its tag explicitly to retry" + ) + if not re.fullmatch(r"v[0-9]+\.[0-9]+\.[0-9]+", tag): raise ValueError("Stable tag must match vMAJOR.MINOR.PATCH") @@ -146,7 +164,7 @@ def select_candidate(tag: str, branch: str) -> dict: def main() -> None: parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("tag") + parser.add_argument("tag", nargs="?", default="") parser.add_argument("--branch", required=True) args = parser.parse_args() print(json.dumps(select_candidate(args.tag, args.branch))) From 43c385d1edd969e4b21e3ed70eb5e9612ee5cf3d Mon Sep 17 00:00:00 2001 From: Simon Scatton Date: Mon, 19 Oct 2026 11:52:09 +0200 Subject: [PATCH 2/2] fix(release): use resolved tag in success notifications Signed-off-by: Simon Scatton --- .github/workflows/release-prerelease.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-prerelease.yml b/.github/workflows/release-prerelease.yml index b67d8b3d6b..5dbb1a446c 100644 --- a/.github/workflows/release-prerelease.yml +++ b/.github/workflows/release-prerelease.yml @@ -147,7 +147,7 @@ jobs: notify-prerelease-success: name: Notify Prerelease Success - needs: [build, publish, qualify] + needs: [resolve, build, publish, qualify] if: success() runs-on: ubuntu-latest timeout-minutes: 2 @@ -156,10 +156,10 @@ jobs: - name: Send Slack notification continue-on-error: true env: - RELEASE_TAG: ${{ inputs.tag || github.ref_name }} + RELEASE_TAG: ${{ needs.resolve.outputs.tag }} SLACK_WEBHOOK_URL: ${{ secrets.SLACK_OPENSHELL_TRIAGE_WEBHOOK_URL }} RUN_URL: ${{ format('{0}/{1}/actions/runs/{2}/attempts/{3}', github.server_url, github.repository, github.run_id, github.run_attempt) }} - TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, inputs.tag || github.ref_name) }} + TAG_URL: ${{ format('{0}/{1}/tree/{2}', github.server_url, github.repository, needs.resolve.outputs.tag) }} run: | set -euo pipefail if [[ -z "${SLACK_WEBHOOK_URL}" ]]; then