ci: resolve physical macOS temporary paths for sync tests #201
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Gitee Mirror | |
| # Mirrors github.com/vnotex/vnote to gitee.com/vnotex/vnote: | |
| # * Job A (sync-code) - pushes master + release tags on every push to master. | |
| # * Job B (mirror-release) - on a published GitHub release (or manual dispatch), | |
| # creates/updates the matching Gitee release, uploads | |
| # the SMALL in-app-update assets, and prunes all but | |
| # the two most recent releases. | |
| # | |
| # The ~158 MB platform ZIPs are NOT mirrored: Gitee's attachment endpoint runs at | |
| # well under 50 KB/s from a GitHub-hosted runner, so the maintainer uploads those | |
| # by hand. Note that those hand-uploaded platform packages are what a user | |
| # following VNote's "Update Available" prompt to the Gitee release page expects | |
| # to find there. | |
| # | |
| # The small update assets (*.manifest.json, *.manifest.json.minisig and the delta | |
| # ZIP) ARE mirrored automatically. The client itself no longer consumes them - | |
| # they are published as the artifact interface for a future EXTERNAL updater (see | |
| # docs/update-signing.md) - but mirroring keeps both forges carrying the same | |
| # release contents. Uploads are additive: | |
| # Gitee has no replace semantics, so an already-attached name is skipped and no | |
| # existing attachment is ever removed. | |
| # | |
| # The `continuous-build` tag/release is deliberately NOT mirrored: it is the only | |
| # mutable tag, CI force-moves it after the push event, and Gitee has no matching | |
| # release. See the job guards below. | |
| # | |
| # Credentials live in the `GiteeMirror` repository environment as the secrets | |
| # GITEE_USERNAME (the Gitee account login/path) and GITEE_TOKEN (a 私人令牌 with | |
| # the `projects` scope). Both jobs must declare `environment:` to see them. | |
| on: | |
| push: | |
| branches: [ master ] | |
| release: | |
| types: [ published ] | |
| workflow_dispatch: | |
| inputs: | |
| tag: | |
| description: 'Release tag to mirror to Gitee (e.g. v4.3.0)' | |
| required: true | |
| permissions: | |
| contents: read | |
| env: | |
| GITEE_OWNER: vnotex | |
| GITEE_REPO: vnote | |
| jobs: | |
| # --------------------------------------------------------------------------- | |
| # Job A - push master + release tags to Gitee. | |
| # --------------------------------------------------------------------------- | |
| sync-code: | |
| if: github.event_name == 'push' | |
| runs-on: ubuntu-latest | |
| environment: GiteeMirror | |
| timeout-minutes: 30 | |
| concurrency: | |
| group: gitee-mirror-code | |
| cancel-in-progress: true # a newer master push supersedes an older sync | |
| env: | |
| GITEE_USERNAME: ${{ secrets.GITEE_USERNAME }} | |
| GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} | |
| steps: | |
| - name: Mask credentials | |
| run: | | |
| set -euo pipefail | |
| echo "::add-mask::$GITEE_TOKEN" | |
| echo "::add-mask::$GITEE_USERNAME" | |
| test -n "$GITEE_TOKEN" || { echo "GITEE_TOKEN is empty" >&2; exit 1; } | |
| test -n "$GITEE_USERNAME" || { echo "GITEE_USERNAME is empty" >&2; exit 1; } | |
| - uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # full history | |
| fetch-tags: true # ... and tags; required by the refs/tags/v* push | |
| submodules: false # D8: submodules keep pointing at GitHub | |
| - name: Push master and release tags to Gitee | |
| run: | | |
| set -euo pipefail | |
| echo "::add-mask::$GITEE_TOKEN" | |
| . .github/scripts/gitee-lib.sh | |
| git push "$REMOTE" refs/heads/master:refs/heads/master | |
| # Release tags only. `continuous-build` (the sole mutable tag) is excluded: | |
| # CI force-moves it AFTER the push event using GITHUB_TOKEN, so any | |
| # push-time mirror would be stale, and a plain `git push --tags` would | |
| # hard-fail on it forever. | |
| # | |
| # Fast path: one batched round trip. `refs/tags/v*:refs/tags/v*` is a valid | |
| # git pattern refspec (exactly one `*` on each side); quote it so the shell | |
| # does not glob it. | |
| if ! git push "$REMOTE" "refs/tags/v*:refs/tags/v*"; then | |
| echo "batched tag push failed; falling back to per-tag pushes" | |
| # Per-tag, so a tag Job B pushed concurrently does not fail the sync. | |
| # NOT the default: this is ~90 round trips. | |
| git tag -l 'v*' | while read -r t; do | |
| push_tag_idempotent "$t" "$(git rev-parse "refs/tags/$t^{commit}")" || exit 1 | |
| done | |
| fi | |
| # --------------------------------------------------------------------------- | |
| # Job B - mirror one published release to Gitee. | |
| # --------------------------------------------------------------------------- | |
| mirror-release: | |
| if: >- | |
| github.event_name == 'workflow_dispatch' || | |
| (github.event_name == 'release' && | |
| !github.event.release.draft && | |
| !github.event.release.prerelease && | |
| github.event.release.tag_name != 'continuous-build') | |
| runs-on: ubuntu-latest | |
| environment: GiteeMirror | |
| timeout-minutes: 30 | |
| concurrency: | |
| group: gitee-mirror-release | |
| cancel-in-progress: false | |
| # `single` (the default) would cancel a PENDING release run when a second | |
| # one arrives. `max` keeps up to 100 pending runs instead. It may not be | |
| # combined with `cancel-in-progress: true`. | |
| # NOTE: actionlint <=1.7.12 does not know this key yet and reports | |
| # `unexpected key "queue"`. That is a stale-schema false positive; `queue` | |
| # is documented current GitHub Actions syntax. Do not remove it. | |
| queue: max | |
| env: | |
| GITEE_USERNAME: ${{ secrets.GITEE_USERNAME }} | |
| GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| GH_REPO: ${{ github.repository }} | |
| steps: | |
| - name: Mask credentials | |
| run: | | |
| set -euo pipefail | |
| echo "::add-mask::$GITEE_TOKEN" | |
| echo "::add-mask::$GITEE_USERNAME" | |
| test -n "$GITEE_TOKEN" || { echo "GITEE_TOKEN is empty" >&2; exit 1; } | |
| test -n "$GITEE_USERNAME" || { echo "GITEE_USERNAME is empty" >&2; exit 1; } | |
| - name: Resolve tag | |
| env: | |
| # Indirection through env: never interpolate event/input data straight | |
| # into a shell script. | |
| INPUT_TAG: ${{ inputs.tag }} | |
| RELEASE_TAG: ${{ github.event.release.tag_name }} | |
| run: | | |
| set -euo pipefail | |
| TAG="${INPUT_TAG:-}" | |
| if [ -z "$TAG" ]; then TAG="${RELEASE_TAG:-}"; fi | |
| if [ -z "$TAG" ]; then | |
| echo "could not resolve a tag to mirror" >&2 | |
| exit 1 | |
| fi | |
| if [ "$TAG" = "continuous-build" ]; then | |
| echo "refusing to mirror the continuous-build tag" >&2 | |
| exit 1 | |
| fi | |
| case "$TAG" in | |
| *[!A-Za-z0-9._/-]*) | |
| echo "tag contains unexpected characters: $TAG" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| echo "TAG=$TAG" >> "$GITHUB_ENV" | |
| echo "mirroring tag: $TAG" | |
| # On a `release` event github.ref is refs/tags/<tag>, so the default checkout | |
| # is a detached tag with no local master branch. Pin master explicitly; that | |
| # also removes the manual-dispatch ref ambiguity. | |
| - uses: actions/checkout@v4 | |
| with: | |
| ref: master | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| submodules: false | |
| - name: Fetch GitHub release metadata | |
| run: | | |
| set -euo pipefail | |
| gh release view "$TAG" \ | |
| --json tagName,name,body,isDraft,isPrerelease,assets \ | |
| > "$RUNNER_TEMP/release.json" | |
| if [ "$(jq -r '.isDraft' "$RUNNER_TEMP/release.json")" = "true" ]; then | |
| echo "release $TAG is still a draft; refusing to mirror" >&2 | |
| exit 1 | |
| fi | |
| if [ "$(jq -r '.isPrerelease' "$RUNNER_TEMP/release.json")" = "true" ]; then | |
| echo "release $TAG is a prerelease; refusing to mirror" >&2 | |
| exit 1 | |
| fi | |
| NAME="$(jq -r '.name // ""' "$RUNNER_TEMP/release.json")" | |
| [ -n "$NAME" ] || NAME="$TAG" | |
| printf '%s' "$NAME" > "$RUNNER_TEMP/name.txt" | |
| # Gitee requires a non-empty body. | |
| BODY="$(jq -r '.body // ""' "$RUNNER_TEMP/release.json")" | |
| [ -n "$BODY" ] || BODY="$NAME" | |
| printf '%s' "$BODY" > "$RUNNER_TEMP/body.txt" | |
| echo "release name: $NAME" | |
| echo "assets on GitHub:" | |
| jq -r '.assets[].name' "$RUNNER_TEMP/release.json" || true | |
| - name: Push the tag to Gitee | |
| run: | | |
| set -euo pipefail | |
| echo "::add-mask::$GITEE_TOKEN" | |
| . .github/scripts/gitee-lib.sh | |
| # Belt and braces: `ref: master` checks out a branch, and the tag may | |
| # have been created after that ref was resolved. Fetch it explicitly | |
| # instead of trusting checkout's tag handling. | |
| git fetch --force origin "+refs/tags/${TAG}:refs/tags/${TAG}" | |
| SHA="$(git rev-parse "refs/tags/$TAG^{commit}")" | |
| echo "SHA=$SHA" >> "$GITHUB_ENV" | |
| # Only the tag - Job A owns refs/heads/master, and pushing a tag ref | |
| # transfers its commit plus every missing reachable object, so $SHA is | |
| # resolvable on Gitee (needed for target_commitish) even when Gitee's | |
| # master has not advanced yet. | |
| push_tag_idempotent "$TAG" "$SHA" | |
| - name: Create or update the Gitee release | |
| run: | | |
| set -euo pipefail | |
| echo "::add-mask::$GITEE_TOKEN" | |
| . .github/scripts/gitee-lib.sh | |
| NAME="$(cat "$RUNNER_TEMP/name.txt")" | |
| BODY="$(cat "$RUNNER_TEMP/body.txt")" | |
| # The ~158 MB platform ZIPs are NOT mirrored. Gitee's attach_files | |
| # endpoint sustained well under 50 KB/s from a GitHub-hosted runner | |
| # (run 30447854128: 55 minutes without finishing the first 164 MiB | |
| # asset, against a ~400 MiB payload), so the maintainer attaches | |
| # those by hand. The small in-app-update assets ARE mirrored, by the | |
| # step right after this one. Uploads are additive, so this job can | |
| # never remove an attachment from a release it keeps. (Pruning an | |
| # OLDER release naturally takes that release's attachments with it.) | |
| # NOTE: Gitee answers GET /releases/tags/{tag} with 200 + a literal | |
| # `null` body when the release is absent; it never 404s. That is why | |
| # existence is decided by gitee_find_release_id, not by the status | |
| # code, and why the id is always re-resolved by lookup instead of | |
| # being read out of a POST response. | |
| if gitee_find_release_id "$TAG"; then | |
| RELEASE_ID="$FOUND_RELEASE_ID" | |
| echo "gitee release for $TAG already exists (id=$RELEASE_ID); updating" | |
| gitee_patch_release "$RELEASE_ID" "$TAG" "$NAME" "$BODY" | |
| api_require_2xx "update release" | |
| else | |
| lookup_rc=$? | |
| if [ "$lookup_rc" -ne 1 ]; then | |
| echo "looking up the release by tag failed (HTTP $API_CODE)" >&2 | |
| printf '%s\n' "$API_BODY" >&2 | |
| exit 1 | |
| fi | |
| # Absent. Create, then re-resolve authoritatively. A definite | |
| # "still absent" proves the POST never landed, so exactly one | |
| # re-POST is safe; anything else is a hard failure. | |
| RELEASE_ID="" | |
| attempt=1 | |
| while [ "$attempt" -le 2 ]; do | |
| echo "creating gitee release for $TAG (attempt $attempt)" | |
| gitee_create_release "$TAG" "$NAME" "$BODY" "$SHA" | |
| create_code="${API_CODE:-000}" | |
| if gitee_find_release_id "$TAG"; then | |
| RELEASE_ID="$FOUND_RELEASE_ID" | |
| echo "gitee release created (id=$RELEASE_ID)" | |
| break | |
| else | |
| lookup_rc=$? | |
| if [ "$lookup_rc" -ne 1 ]; then | |
| echo "looking up the release after create failed (HTTP $API_CODE)" >&2 | |
| printf '%s\n' "$API_BODY" >&2 | |
| exit 1 | |
| fi | |
| echo "release still absent (create returned HTTP $create_code)" | |
| printf '%s\n' "$API_BODY" >&2 | |
| fi | |
| attempt=$((attempt + 1)) | |
| done | |
| if [ -z "$RELEASE_ID" ]; then | |
| echo "failed to create the gitee release for $TAG" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| if [ -z "$RELEASE_ID" ] || [ "$RELEASE_ID" = "null" ]; then | |
| echo "could not determine the gitee release id" >&2 | |
| exit 1 | |
| fi | |
| echo "RELEASE_ID=$RELEASE_ID" >> "$GITHUB_ENV" | |
| echo "gitee release id: $RELEASE_ID" | |
| - name: Mirror the in-app update assets to Gitee | |
| run: | | |
| set -euo pipefail | |
| echo "::add-mask::$GITEE_TOKEN" | |
| . .github/scripts/gitee-lib.sh | |
| # Only the SMALL update assets. The platform ZIPs stay manual (see the | |
| # header comment): a few MB at Gitee's measured throughput is fine, a | |
| # few hundred is not. | |
| # | |
| # Why this matters: UpdateService needs <name>.manifest.json plus its | |
| # detached .minisig to plan and verify anything. Without them a Gitee | |
| # user silently degrades to check-only updates, so a failed upload is | |
| # a HARD failure of this step rather than a warning. | |
| ASSET_DIR="$RUNNER_TEMP/update-assets" | |
| rm -rf "$ASSET_DIR" | |
| mkdir -p "$ASSET_DIR" | |
| # Decide "there is nothing to mirror" from a SUCCESSFUL metadata query, | |
| # never from a failing download. `gh release download` exits non-zero | |
| # for auth failures, rate limits, network errors and disk errors just | |
| # as it does for "no assets matched", so treating any failure as a | |
| # skip would leave Gitee without a manifest while the job stays green. | |
| jq -r 'if (.assets | type) == "array" then .assets[].name | |
| else error("expected an assets array") end' \ | |
| "$RUNNER_TEMP/release.json" > "$RUNNER_TEMP/all-assets.txt" | |
| # grep exit 1 is "no match", a legitimate answer here. Exit >= 2 is a | |
| # real error (unreadable input, bad pattern), and MUST NOT be laundered | |
| # into a green "nothing to mirror" skip. | |
| grep_rc=0 | |
| grep -E '(\.manifest\.json|\.manifest\.json\.minisig|\.delta\.zip)$' \ | |
| "$RUNNER_TEMP/all-assets.txt" > "$RUNNER_TEMP/wanted-assets.txt" || grep_rc=$? | |
| if [ "$grep_rc" -gt 1 ]; then | |
| echo "filtering the GitHub asset list failed (grep exit $grep_rc)" >&2 | |
| exit 1 | |
| fi | |
| if [ ! -s "$RUNNER_TEMP/wanted-assets.txt" ]; then | |
| echo "the GitHub release for $TAG publishes no in-app update assets; skipping" | |
| exit 0 | |
| fi | |
| echo "in-app update assets on the GitHub release:" | |
| cat "$RUNNER_TEMP/wanted-assets.txt" | |
| # Assets DO exist, so from here every failure is a real failure. | |
| gh release download "$TAG" --dir "$ASSET_DIR" --clobber \ | |
| --pattern '*.manifest.json' \ | |
| --pattern '*.manifest.json.minisig' \ | |
| --pattern '*.delta.zip' | |
| # Sorted so the log is stable across runs. | |
| : > "$RUNNER_TEMP/update-assets.txt" | |
| find "$ASSET_DIR" -maxdepth 1 -type f -print | sort > "$RUNNER_TEMP/update-assets.txt" | |
| # A partial download must not silently mirror a manifest without its | |
| # signature (or the other way round). | |
| sed 's|.*/||' "$RUNNER_TEMP/update-assets.txt" | sort > "$RUNNER_TEMP/got-assets.txt" | |
| sort "$RUNNER_TEMP/wanted-assets.txt" > "$RUNNER_TEMP/wanted-assets-sorted.txt" | |
| if ! diff -u "$RUNNER_TEMP/wanted-assets-sorted.txt" "$RUNNER_TEMP/got-assets.txt"; then | |
| echo "downloaded update assets do not match the release listing" >&2 | |
| exit 1 | |
| fi | |
| # Gitee has NO replace semantics for attachments, so an existing name | |
| # must be skipped rather than re-posted (which would silently produce | |
| # a duplicate). List what is already attached first. | |
| api GET "/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/${RELEASE_ID}/attach_files" \ | |
| --retry 3 --retry-delay 5 | |
| if ! api_is_2xx; then | |
| echo "listing gitee release attachments failed (HTTP ${API_CODE:-000})" >&2 | |
| printf '%s\n' "$API_BODY" >&2 | |
| exit 1 | |
| fi | |
| # A 2xx that is not an array is a protocol surprise, NOT "no | |
| # attachments": accepting it would re-upload everything as duplicates. | |
| if ! printf '%s' "$API_BODY" \ | |
| | jq -r 'if type == "array" then .[].name | |
| else error("expected an array of attachments") end' \ | |
| > "$RUNNER_TEMP/attached.txt"; then | |
| echo "could not parse the gitee attachment listing" >&2 | |
| printf '%s\n' "$API_BODY" >&2 | |
| exit 1 | |
| fi | |
| while read -r path; do | |
| [ -n "$path" ] || continue | |
| name="$(basename "$path")" | |
| if grep -Fxq -- "$name" "$RUNNER_TEMP/attached.txt"; then | |
| echo "already attached on gitee: $name (skipping)" | |
| continue | |
| fi | |
| echo "uploading to gitee: $name ($(wc -c < "$path") bytes)" | |
| api POST "/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/${RELEASE_ID}/attach_files" \ | |
| -F "file=@${path}" | |
| api_require_2xx "attach $name" | |
| done < "$RUNNER_TEMP/update-assets.txt" | |
| echo "update assets mirrored" | |
| - name: Prune older Gitee releases | |
| run: | | |
| set -euo pipefail | |
| echo "::add-mask::$GITEE_TOKEN" | |
| . .github/scripts/gitee-lib.sh | |
| # D7a: prune ONLY when the mirrored tag is GitHub's current latest stable | |
| # release, so a dispatch for an older tag can never delete a newer Gitee | |
| # release. /releases/latest already excludes drafts and prereleases. | |
| # | |
| # A failed lookup must NOT be downgraded to "skip prune, exit 0": that | |
| # would leave surplus Gitee releases while reporting success and hide | |
| # the need for a rerun. Fail red instead - nothing has been deleted at | |
| # this point, so a rerun is safe. | |
| if ! LATEST="$(gh api "repos/$GH_REPO/releases/latest" --jq .tag_name)"; then | |
| echo "could not query GitHub's latest release; refusing to prune" >&2 | |
| exit 1 | |
| fi | |
| if [ -z "$LATEST" ]; then | |
| echo "GitHub's latest release has an empty tag_name; refusing to prune" >&2 | |
| exit 1 | |
| fi | |
| if [ "$LATEST" != "$TAG" ]; then | |
| echo "GitHub's latest release is $LATEST, not $TAG; skipping prune" | |
| exit 0 | |
| fi | |
| # Collect every id FIRST. Deleting while paginating shifts pages and | |
| # silently skips releases. Store `id<TAB>created_at` so the keep set | |
| # can be chosen by recency. | |
| : > "$RUNNER_TEMP/releases.tsv" | |
| page=1 | |
| while : ; do | |
| api GET "/repos/${GITEE_OWNER}/${GITEE_REPO}/releases?page=${page}&per_page=100" \ | |
| --retry 3 --retry-delay 5 | |
| if [ "$API_CODE" != "200" ]; then | |
| echo "listing gitee releases failed (HTTP $API_CODE)" >&2 | |
| printf '%s\n' "$API_BODY" >&2 | |
| exit 1 | |
| fi | |
| n="$(printf '%s' "$API_BODY" | jq 'length')" | |
| if [ "$n" -eq 0 ]; then break; fi | |
| printf '%s' "$API_BODY" \ | |
| | jq -r '.[] | [(.id|tostring), (.created_at // "")] | @tsv' \ | |
| >> "$RUNNER_TEMP/releases.tsv" | |
| if [ "$n" -lt 100 ]; then break; fi | |
| page=$((page + 1)) | |
| done | |
| cut -f1 "$RUNNER_TEMP/releases.tsv" > "$RUNNER_TEMP/release-ids.txt" | |
| count="$(grep -Fxc -- "$RELEASE_ID" "$RUNNER_TEMP/release-ids.txt" || true)" | |
| if [ "${count:-0}" != "1" ]; then | |
| echo "expected the mirrored release id $RELEASE_ID exactly once in the listing, got ${count:-0}" >&2 | |
| exit 1 | |
| fi | |
| # D7: Gitee keeps the TWO most recent releases. The mirrored one is | |
| # always kept; the second slot goes to the newest of the others. | |
| # | |
| # Recency is keyed on the release id, NOT created_at: Gitee assigns | |
| # ids from a monotonically increasing counter, and unlike created_at | |
| # the id is always present and always numeric. Sorting on created_at | |
| # would be actively unsafe with mixed data - an empty timestamp sorts | |
| # last under `-k2,2r`, so a higher-id release lacking a timestamp | |
| # would be deleted rather than kept. | |
| # | |
| # created_at is used only to CROSS-CHECK the choice: if some other | |
| # candidate claims a strictly newer timestamp, the two signals | |
| # disagree and we fail closed before deleting anything. | |
| # | |
| # Every step writes to a file rather than piping into `head`: under | |
| # `set -o pipefail`, `head` closing the pipe early makes the producer | |
| # exit 141 (SIGPIPE) once the output outgrows the pipe buffer. | |
| awk -F'\t' -v self="$RELEASE_ID" '$1 != self' \ | |
| "$RUNNER_TEMP/releases.tsv" > "$RUNNER_TEMP/others.tsv" | |
| sort -t"$(printf '\t')" -k1,1nr "$RUNNER_TEMP/others.tsv" \ | |
| > "$RUNNER_TEMP/others_sorted.tsv" | |
| KEEP_SECOND="$(head -n 1 "$RUNNER_TEMP/others_sorted.tsv" | cut -f1)" | |
| if [ -n "$KEEP_SECOND" ]; then | |
| KEEP_SECOND_AT="$(awk -F'\t' -v id="$KEEP_SECOND" \ | |
| '$1 == id { print $2; exit }' "$RUNNER_TEMP/others_sorted.tsv")" | |
| if [ -n "$KEEP_SECOND_AT" ]; then | |
| NEWER="$(awk -F'\t' -v id="$KEEP_SECOND" -v at="$KEEP_SECOND_AT" \ | |
| '$1 != id && $2 != "" && $2 > at { print $1; exit }' \ | |
| "$RUNNER_TEMP/others_sorted.tsv")" | |
| if [ -n "$NEWER" ]; then | |
| echo "id order and created_at disagree: release $NEWER is newer than the chosen $KEEP_SECOND ($KEEP_SECOND_AT); refusing to prune" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| echo "keeping gitee releases: $RELEASE_ID (mirrored) and $KEEP_SECOND (previous)" | |
| else | |
| echo "keeping gitee release: $RELEASE_ID (mirrored); no other release exists" | |
| fi | |
| # Read the ids from a file, not `< <(cut ...)`: mapfile reports its | |
| # own status, so a failing process substitution would silently yield | |
| # a short delete set. | |
| mapfile -t ids < "$RUNNER_TEMP/release-ids.txt" | |
| for id in ${ids[@]+"${ids[@]}"}; do | |
| [ -n "$id" ] || continue | |
| [ "$id" != "$RELEASE_ID" ] || continue | |
| [ "$id" != "$KEEP_SECOND" ] || continue | |
| echo "deleting older gitee release id=$id" | |
| api DELETE "/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/${id}" | |
| api_require_2xx "delete release $id" | |
| done | |
| # Attachments on the two retained releases are never touched by the | |
| # prune: this step calls no attachment endpoint at all, and the | |
| # mirroring step above is strictly additive. Deleting an older release | |
| # does of course remove that release and whatever was attached to it. | |
| echo "prune done; git tags are untouched" |