Merge pull request 'fix(ci): preflight release credentials and recover from an orphan tag' (#2) from fix/ci-credential-preflight into main
Reviewed-on: #2 Reviewed-by: Colby Whitlock <whitlocktech@gmail.com>
This commit is contained in:
@@ -79,6 +79,8 @@ jobs:
|
|||||||
# ── RELEASE ENGINE: decide the next version + changelog ──────────────
|
# ── RELEASE ENGINE: decide the next version + changelog ──────────────
|
||||||
- name: Plan the release (version + changelog)
|
- name: Plan the release (version + changelog)
|
||||||
id: plan
|
id: plan
|
||||||
|
env:
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
run: |
|
run: |
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
mkdir -p dist
|
mkdir -p dist
|
||||||
@@ -124,21 +126,60 @@ jobs:
|
|||||||
VERSION="$(bump "${LAST_TAG#v}" "$BUMP")"
|
VERSION="$(bump "${LAST_TAG#v}" "$BUMP")"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# An existing tag is NOT automatically "nothing to do". A tag with no
|
||||||
|
# release behind it means a previous run tagged and then died before
|
||||||
|
# publishing — which is exactly what happened on servuo-plugins' first
|
||||||
|
# release, where absent REGISTRY_* secrets took the release API call to
|
||||||
|
# 401 after the tag had already been pushed. Standing down on the tag
|
||||||
|
# alone makes that state permanent: every later run sees the tag, sets
|
||||||
|
# RELEASE=false, and the release never appears. Note this deliberately
|
||||||
|
# OVERRIDES the RELEASE=false decided just above — with the tag in
|
||||||
|
# place there are no releasable commits after it, so the normal path
|
||||||
|
# would stand down, which is exactly why it could never self-heal.
|
||||||
|
REUSE_TAG=false
|
||||||
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
if git rev-parse -q --verify "refs/tags/v${VERSION}" >/dev/null; then
|
||||||
echo "Tag v${VERSION} already exists — nothing to release."
|
REL_HTTP="$(curl -s -o /dev/null -w '%{http_code}' \
|
||||||
|
-H "Authorization: token $(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" \
|
||||||
|
"https://${GITEA_HOST}/api/v1/repos/${REPO}/releases/tags/v${VERSION}" || echo 000)"
|
||||||
|
if [ "$REL_HTTP" = "200" ]; then
|
||||||
|
echo "Tag v${VERSION} already has a release — nothing to do."
|
||||||
RELEASE=false
|
RELEASE=false
|
||||||
|
elif [ "$REL_HTTP" = "404" ]; then
|
||||||
|
echo "::warning::Tag v${VERSION} exists but has no release — a previous run failed after tagging. Reusing the tag and publishing the release it is missing."
|
||||||
|
REUSE_TAG=true
|
||||||
|
RELEASE=true
|
||||||
|
else
|
||||||
|
# Anything else (000 from a network failure, 401/403 from a bad
|
||||||
|
# token) is not evidence of absence. Guessing "no release" would
|
||||||
|
# re-publish over a good one, so refuse instead.
|
||||||
|
echo "::error::Could not determine whether a release exists for v${VERSION} (HTTP ${REL_HTTP}). Refusing to guess."
|
||||||
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Changelog range. A recovery run has nothing after the tag, so
|
||||||
|
# summarize what the tag itself contains rather than emitting an empty
|
||||||
|
# list: the range that produced it, i.e. previous-tag..this-tag.
|
||||||
|
if [ "$REUSE_TAG" = true ]; then
|
||||||
|
PREV_TAG="$(git describe --tags --match 'v*' --abbrev=0 "v${VERSION}^" 2>/dev/null || true)"
|
||||||
|
if [ -n "$PREV_TAG" ]; then CL_RANGE="${PREV_TAG}..v${VERSION}"; else CL_RANGE="v${VERSION}"; fi
|
||||||
|
SINCE="$PREV_TAG"
|
||||||
|
else
|
||||||
|
CL_RANGE="$RANGE"
|
||||||
|
SINCE="$LAST_TAG"
|
||||||
|
fi
|
||||||
|
CL_SUBJECTS="$(git log --no-merges --format='%s' $CL_RANGE || true)"
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "## ${BIN} v${VERSION}"
|
echo "## ${BIN} v${VERSION}"
|
||||||
echo
|
echo
|
||||||
FEATS="$(echo "$SUBJECTS" | grep -E '^feat' || true)"
|
FEATS="$(echo "$CL_SUBJECTS" | grep -E '^feat' || true)"
|
||||||
FIXES="$(echo "$SUBJECTS" | grep -E '^(fix|perf)' || true)"
|
FIXES="$(echo "$CL_SUBJECTS" | grep -E '^(fix|perf)' || true)"
|
||||||
[ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; }
|
[ -n "$FEATS" ] && { echo "### Features"; echo "$FEATS" | sed 's/^/- /'; echo; }
|
||||||
[ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; }
|
[ -n "$FIXES" ] && { echo "### Fixes"; echo "$FIXES" | sed 's/^/- /'; echo; }
|
||||||
echo "### All changes"
|
echo "### All changes"
|
||||||
if [ -n "$LAST_TAG" ]; then echo "Since ${LAST_TAG}:"; fi
|
if [ -n "$SINCE" ]; then echo "Since ${SINCE}:"; fi
|
||||||
echo "$SUBJECTS" | sed 's/^/- /'
|
echo "$CL_SUBJECTS" | sed 's/^/- /'
|
||||||
echo
|
echo
|
||||||
echo "### Verifying this download"
|
echo "### Verifying this download"
|
||||||
echo
|
echo
|
||||||
@@ -159,7 +200,37 @@ jobs:
|
|||||||
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "tag=v${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
echo "release=${RELEASE}" >> "$GITHUB_OUTPUT"
|
echo "release=${RELEASE}" >> "$GITHUB_OUTPUT"
|
||||||
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
|
echo "bump=${BUMP}" >> "$GITHUB_OUTPUT"
|
||||||
echo "==> release=${RELEASE} version=${VERSION} bump=${BUMP} last_tag=${LAST_TAG:-<none>}"
|
echo "reuse_tag=${REUSE_TAG}" >> "$GITHUB_OUTPUT"
|
||||||
|
echo "==> release=${RELEASE} version=${VERSION} bump=${BUMP} reuse_tag=${REUSE_TAG} last_tag=${LAST_TAG:-<none>}"
|
||||||
|
|
||||||
|
# ── Credential preflight ─────────────────────────────────────────────
|
||||||
|
# Runs BEFORE anything is built or pushed, and only when this run intends
|
||||||
|
# to publish, so a docs:/chore:-only merge (or the pre-crate no-op) stays
|
||||||
|
# green on a repo with no secrets.
|
||||||
|
#
|
||||||
|
# Learned from servuo-plugins' first release: REGISTRY_USER and
|
||||||
|
# REGISTRY_TOKEN were empty, but the tag push SUCCEEDED anyway, because
|
||||||
|
# actions/checkout leaves an `http.<host>.extraheader` credential in the
|
||||||
|
# local git config — so `git remote set-url` to a URL with empty
|
||||||
|
# credentials still authenticated through that leftover header. The
|
||||||
|
# release API call had no such fallback and 401'd, leaving the repo tagged
|
||||||
|
# but unreleased. Checking up front makes that an immediate, legible
|
||||||
|
# failure instead of a half-published release.
|
||||||
|
- name: Verify release credentials are configured
|
||||||
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
||||||
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
MISSING=""
|
||||||
|
[ -n "$(printf '%s' "${REGISTRY_USER:-}" | tr -d '\r\n')" ] || MISSING="${MISSING} REGISTRY_USER"
|
||||||
|
[ -n "$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" ] || MISSING="${MISSING} REGISTRY_TOKEN"
|
||||||
|
if [ -n "$MISSING" ]; then
|
||||||
|
echo "::error::Missing Actions secret(s):${MISSING}. Set them under Settings → Actions → Secrets on ${REPO}. REGISTRY_TOKEN needs the write:repository scope to push the bump commit, the tag, and create the release."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Release credentials present."
|
||||||
|
|
||||||
# ── RUST ADAPTER: toolchain + cross-compile deps ─────────────────────
|
# ── RUST ADAPTER: toolchain + cross-compile deps ─────────────────────
|
||||||
- name: Install Rust toolchain, Windows target, and MinGW linker
|
- name: Install Rust toolchain, Windows target, and MinGW linker
|
||||||
@@ -256,7 +327,16 @@ jobs:
|
|||||||
else
|
else
|
||||||
echo "Version unchanged (first release) — no bump commit needed."
|
echo "Version unchanged (first release) — no bump commit needed."
|
||||||
fi
|
fi
|
||||||
|
# The tag may already exist when finishing a run that died after
|
||||||
|
# tagging (see the plan step). `git tag` on an existing name fails
|
||||||
|
# under `set -e`; pushing an identical existing tag is a harmless
|
||||||
|
# no-op. A push that fails here means the remote tag points somewhere
|
||||||
|
# else, which SHOULD stop the run.
|
||||||
|
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
||||||
|
echo "Tag ${TAG} already exists — reusing it."
|
||||||
|
else
|
||||||
git tag "${TAG}"
|
git tag "${TAG}"
|
||||||
|
fi
|
||||||
git push origin "${TAG}"
|
git push origin "${TAG}"
|
||||||
|
|
||||||
# ── RELEASE ENGINE: create the Gitea release + upload assets ─────────
|
# ── RELEASE ENGINE: create the Gitea release + upload assets ─────────
|
||||||
|
|||||||
@@ -58,6 +58,24 @@ jobs:
|
|||||||
echo "----- generated ${DOCS_PATH} -----"
|
echo "----- generated ${DOCS_PATH} -----"
|
||||||
cat _sync/PROJECT_TREE.md
|
cat _sync/PROJECT_TREE.md
|
||||||
|
|
||||||
|
# Checked explicitly because the failure mode otherwise is a `git clone`
|
||||||
|
# against `https://:@host/...`, whose error says nothing about a missing
|
||||||
|
# secret. This workflow's first run on `main` failed exactly that way.
|
||||||
|
- name: Verify docs-repo credentials are configured
|
||||||
|
env:
|
||||||
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
||||||
|
run: |
|
||||||
|
set -euo pipefail
|
||||||
|
MISSING=""
|
||||||
|
[ -n "$(printf '%s' "${REGISTRY_USER:-}" | tr -d '\r\n')" ] || MISSING="${MISSING} REGISTRY_USER"
|
||||||
|
[ -n "$(printf '%s' "${REGISTRY_TOKEN:-}" | tr -d '\r\n')" ] || MISSING="${MISSING} REGISTRY_TOKEN"
|
||||||
|
if [ -n "$MISSING" ]; then
|
||||||
|
echo "::error::Missing Actions secret(s):${MISSING}. Set them under Settings → Actions → Secrets on ${SELF_REPO}. The token needs read/write on ${DOCS_REPO} to push the branch and open the PR."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Docs-repo credentials present."
|
||||||
|
|
||||||
- name: Open or update the docs PR if the tree changed
|
- name: Open or update the docs PR if the tree changed
|
||||||
env:
|
env:
|
||||||
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
||||||
|
|||||||
Reference in New Issue
Block a user