From ade4bd86753201cd8f1f266d9b15c15554fd1ef4 Mon Sep 17 00:00:00 2001 From: cucadmuh <49571317+cucadmuh@users.noreply.github.com> Date: Fri, 1 May 2026 23:21:57 +0300 Subject: [PATCH] fix(release): restore github-script release creation flow (#412) Use GitHub REST create/update via actions/github-script with target_commitish and release id from API responses to avoid tag lookup races in the gh CLI path. --- .../workflows/reusable-channel-publish.yml | 144 +++++++++--------- 1 file changed, 76 insertions(+), 68 deletions(-) diff --git a/.github/workflows/reusable-channel-publish.yml b/.github/workflows/reusable-channel-publish.yml index 1960825c..74988f4e 100644 --- a/.github/workflows/reusable-channel-publish.yml +++ b/.github/workflows/reusable-channel-publish.yml @@ -148,86 +148,94 @@ jobs: echo "$EOF_MARKER" >> "$GITHUB_OUTPUT" - name: create or update release id: create_release + uses: actions/github-script@v9 env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PACKAGE_VERSION: ${{ steps.get-version.outputs.version }} RELEASE_TAG: ${{ steps.tag.outputs.value }} RELEASE_COMMIT_SHA: ${{ steps.source-sha.outputs.value }} CHANGELOG_BODY: ${{ steps.changelog.outputs.body }} IS_PRERELEASE: ${{ inputs.prerelease }} IS_DRAFT: ${{ inputs.draft_release }} - run: | - set -euo pipefail + with: + script: | + const tag = process.env.RELEASE_TAG; + const targetCommitish = process.env.RELEASE_COMMIT_SHA; + const version = process.env.PACKAGE_VERSION; + const body = process.env.CHANGELOG_BODY || "See the assets to download this version and install."; + const prerelease = process.env.IS_PRERELEASE === "true"; + const draft = process.env.IS_DRAFT === "true"; + const name = `Psysonic v${version}`; - TAG="${RELEASE_TAG}" - TARGET_COMMITISH="${RELEASE_COMMIT_SHA}" - RELEASE_NAME="Psysonic v${PACKAGE_VERSION}" - BODY_FILE="$(mktemp)" - printf '%s' "${CHANGELOG_BODY}" > "$BODY_FILE" + if (!tag || !/^app-v\d+\.\d+\.\d+(?:[-+].+)?$/.test(tag)) { + throw new Error(`Invalid RELEASE_TAG '${tag ?? ""}'`); + } + if (!targetCommitish || !/^[0-9a-f]{40}$/i.test(targetCommitish)) { + throw new Error(`Invalid RELEASE_COMMIT_SHA '${targetCommitish ?? ""}'`); + } - if [[ -z "${TAG:-}" || ! "$TAG" =~ ^app-v[0-9]+\.[0-9]+\.[0-9]+ ]]; then - echo "::error::Invalid RELEASE_TAG '$TAG'" - exit 1 - fi - if [[ -z "${TARGET_COMMITISH:-}" || ! "$TARGET_COMMITISH" =~ ^[0-9a-fA-F]{40}$ ]]; then - echo "::error::Invalid RELEASE_COMMIT_SHA '$TARGET_COMMITISH'" - exit 1 - fi + // Wait until the tags/ ref is visible in REST API. + const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); + let tagVisible = false; + for (let attempt = 1; attempt <= 10; attempt++) { + try { + await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: `tags/${tag}`, + }); + tagVisible = true; + break; + } catch (e) { + if (e.status !== 404) throw e; + core.warning(`Tag ref '${tag}' not visible in REST API yet (attempt ${attempt}/10); retrying...`); + await sleep(1000 * attempt); + } + } + if (!tagVisible) { + throw new Error(`Tag ref 'tags/${tag}' is still not visible in REST API after retries`); + } - # Wait until GitHub REST sees the tag ref. - TAG_VISIBLE=0 - for attempt in {1..10}; do - if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG}" >/dev/null 2>&1; then - TAG_VISIBLE=1 - break - fi - echo "::warning::Tag ref '${TAG}' not visible in REST API yet (attempt ${attempt}/10); retrying..." - sleep "$attempt" - done - if [[ "$TAG_VISIBLE" -ne 1 ]]; then - echo "::error::Tag ref 'tags/${TAG}' is still not visible in REST API after retries" - exit 1 - fi + let releaseId = null; + try { + const { data } = await github.rest.repos.getReleaseByTag({ + owner: context.repo.owner, + repo: context.repo.repo, + tag, + }); + const { data: updated } = await github.rest.repos.updateRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + release_id: data.id, + tag_name: tag, + target_commitish: targetCommitish, + name, + body, + draft, + prerelease, + }); + releaseId = updated.id; + core.info(`Updated existing release id=${releaseId} for tag ${tag}`); + } catch (e) { + if (e.status !== 404) throw e; + const { data } = await github.rest.repos.createRelease({ + owner: context.repo.owner, + repo: context.repo.repo, + tag_name: tag, + target_commitish: targetCommitish, + name, + body, + draft, + prerelease, + }); + releaseId = data.id; + core.info(`Created release id=${releaseId} for tag ${tag}`); + } - jq -n \ - --arg tag "$TAG" \ - --arg target_commitish "$TARGET_COMMITISH" \ - --arg name "$RELEASE_NAME" \ - --rawfile body "$BODY_FILE" \ - --argjson draft "$([[ "${IS_DRAFT}" == "true" ]] && echo true || echo false)" \ - --argjson prerelease "$([[ "${IS_PRERELEASE}" == "true" ]] && echo true || echo false)" \ - '{tag_name:$tag,target_commitish:$target_commitish,name:$name,body:$body,draft:$draft,prerelease:$prerelease}' > /tmp/release-payload.json + if (!releaseId || !Number.isInteger(Number(releaseId))) { + throw new Error(`create/update release did not produce numeric id for tag '${tag}'`); + } - RELEASE_ID="" - # Resolve existing release by exact tag from list endpoint (more stable than /releases/tags right after create). - RELEASE_ID="$(gh api "repos/${GITHUB_REPOSITORY}/releases?per_page=100" --jq ".[] | select(.tag_name==\"${TAG}\") | .id" | head -n1 || true)" - - if [[ -n "${RELEASE_ID:-}" && "$RELEASE_ID" =~ ^[0-9]+$ ]]; then - gh api --method PATCH "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --input /tmp/release-payload.json >/dev/null - echo "Updated existing release id=${RELEASE_ID} for tag ${TAG}" - else - gh api --method POST "repos/${GITHUB_REPOSITORY}/releases" --input /tmp/release-payload.json >/tmp/release-created.json - RELEASE_ID="$(jq -r '.id // empty' /tmp/release-created.json)" - CREATED_TAG="$(jq -r '.tag_name // empty' /tmp/release-created.json)" - if [[ -z "${RELEASE_ID:-}" || ! "$RELEASE_ID" =~ ^[0-9]+$ ]]; then - echo "::error::Release created but failed to resolve numeric id for tag '${TAG}'" - exit 1 - fi - if [[ "${CREATED_TAG}" != "${TAG}" ]]; then - echo "::error::GitHub created release id=${RELEASE_ID} with tag '${CREATED_TAG}', expected '${TAG}'" - exit 1 - fi - echo "Created release id=${RELEASE_ID} for tag ${TAG}" - fi - - # Verify binding by release id, not by tag endpoint (avoids transient 404 by tag). - TAG_BY_ID="$(gh api "repos/${GITHUB_REPOSITORY}/releases/${RELEASE_ID}" --jq '.tag_name // empty' || true)" - if [[ -z "${TAG_BY_ID:-}" || "${TAG_BY_ID}" != "${TAG}" ]]; then - echo "::error::release_id=${RELEASE_ID} is bound to '${TAG_BY_ID}', expected '${TAG}'" - exit 1 - fi - - echo "release_id=${RELEASE_ID}" >> "$GITHUB_OUTPUT" + core.setOutput("release_id", String(releaseId)); - name: validate release id output env: RELEASE_ID: ${{ steps.create_release.outputs.release_id }}