Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 3a52abbd77 | |||
| eebc74ac8d |
@@ -54,6 +54,10 @@
|
|||||||
# tag and create the release.
|
# tag and create the release.
|
||||||
# REGISTRY_USER — the Gitea username that token belongs to.
|
# REGISTRY_USER — the Gitea username that token belongs to.
|
||||||
#
|
#
|
||||||
|
# These are checked by an explicit preflight step rather than left to fail
|
||||||
|
# wherever they happen to be used first — see the comment on that step for why
|
||||||
|
# an absent token does NOT simply fail the tag push.
|
||||||
|
#
|
||||||
# TODO (Phase 0 item 3): once the installer repo's bundle workflow exists, append
|
# TODO (Phase 0 item 3): once the installer repo's bundle workflow exists, append
|
||||||
# a final step here that POSTs to its workflow-dispatch endpoint, so a new
|
# a final step here that POSTs to its workflow-dispatch endpoint, so a new
|
||||||
# overlay release recomposes the bundle immediately instead of waiting for the
|
# overlay release recomposes the bundle immediately instead of waiting for the
|
||||||
@@ -93,6 +97,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
|
||||||
@@ -129,28 +135,101 @@ 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 the first run here,
|
||||||
|
# when the missing REGISTRY_* secrets took the release API call to 401
|
||||||
|
# after the tag had already been pushed. Standing down on the tag alone
|
||||||
|
# would make that state permanent: every later run would see the tag,
|
||||||
|
# set RELEASE=false, and the release would never appear. So distinguish
|
||||||
|
# the two cases and finish the job the earlier run started.
|
||||||
|
# Note this OVERRIDES the RELEASE=false decided just above. With the tag
|
||||||
|
# already in place there are no releasable commits after it, so the
|
||||||
|
# normal path stands down — which is precisely why the stuck state
|
||||||
|
# could never clear itself. Recovery has to be able to say "yes,
|
||||||
|
# publish" for a version the bump logic considers already done.
|
||||||
|
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" here
|
||||||
|
# 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 "## ${ARTIFACT} v${VERSION}"
|
echo "## ${ARTIFACT} 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/^/- /'
|
||||||
} > dist/CHANGELOG.md
|
} > dist/CHANGELOG.md
|
||||||
|
|
||||||
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
||||||
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 stays green on a repo that has
|
||||||
|
# no secrets.
|
||||||
|
#
|
||||||
|
# This exists because of how the first run failed. REGISTRY_USER and
|
||||||
|
# REGISTRY_TOKEN were empty, but the tag push SUCCEEDED anyway:
|
||||||
|
# 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 returned 401 — so the run
|
||||||
|
# tagged the repo and then failed, which is the worst of both outcomes.
|
||||||
|
# Checking the secrets up front turns that into 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 tag and create the release."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "Release credentials present."
|
||||||
|
|
||||||
- name: Install jq
|
- name: Install jq
|
||||||
if: ${{ steps.plan.outputs.release == 'true' }}
|
if: ${{ steps.plan.outputs.release == 'true' }}
|
||||||
@@ -303,7 +382,18 @@ jobs:
|
|||||||
git config user.email "ci@whitlocktech.com"
|
git config user.email "ci@whitlocktech.com"
|
||||||
git remote set-url origin \
|
git remote set-url origin \
|
||||||
"https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
|
"https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${REPO}.git"
|
||||||
|
|
||||||
|
# The tag may already exist when we are finishing a run that died after
|
||||||
|
# tagging (see the plan step). `git tag` on an existing name fails under
|
||||||
|
# `set -e`, and pushing an identical existing tag is a harmless no-op —
|
||||||
|
# so create it only if it is new, then push either way. 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 ─────────
|
||||||
|
|||||||
Reference in New Issue
Block a user