All checks were successful
PR Checks / rust-gates (pull_request) Successful in 5s
servuo-plugins hit both of these on its first real release run; this repo runs the same engine, so it has the same two defects latent. REGISTRY_USER / REGISTRY_TOKEN were empty there, yet the tag push SUCCEEDED: 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. Net result was the worst available outcome — the repo tagged, no release, and a failed job. Two fixes: A credential preflight, before anything is built or pushed, gated on the run actually intending to publish so a docs:/chore:-only merge (or this repo's pre-crate no-op) still passes on a repo with no secrets. It names the missing secrets and the scope they need instead of failing wherever they happen to be used first. Orphan-tag recovery. A tag with no release behind it means an earlier run died after tagging, and the old code treated any existing tag as "nothing to release" — so that state could never clear itself: every later run would see the tag and stand down, forever. The plan step now asks the API whether a release exists for the tag, and on 404 reuses the tag and publishes the release it is missing. This deliberately overrides the RELEASE=false the bump logic just decided, which is the whole point — with the tag in place there are no releasable commits after it. Anything other than 200/404 (network failure, bad token) is refused rather than guessed, since assuming "no release" would republish over a good one. The tag step now reuses an existing tag instead of failing on `git tag`, and the changelog for a recovery run summarizes what the tag contains (previous-tag..this-tag) rather than the empty range after it. sync-project-tree gets the same preflight: its first run on main failed with an opaque `git clone` error against `https://:@host/...` that said nothing about a missing secret. Verified by extracting every run block and exercising the paths: empty secrets fail the preflight with a legible message and populated ones pass; the no-Cargo.toml guard still short-circuits to release=false; a crate with no tag still takes the seed path; and against real repo state, a tag with a release stands down while an orphan tag recovers. Co-Authored-By: Claude <noreply@anthropic.com>
130 lines
5.8 KiB
YAML
130 lines
5.8 KiB
YAML
name: sync-project-tree
|
|
|
|
# Keeps this repo's file-layout snapshot (docs/installer/PROJECT_TREE.md in the
|
|
# RunicGateway/docs repo) current. On every push to `main` it regenerates the
|
|
# tree from tracked files and, if it changed, opens (or force-updates) a pull
|
|
# request against the docs repo. It never writes to the docs repo's `main`
|
|
# directly. Auth reuses the same REGISTRY_USER / REGISTRY_TOKEN secrets the
|
|
# other workflows use (the token needs repo read/write on RunicGateway/docs).
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
workflow_dispatch: {}
|
|
|
|
concurrency:
|
|
group: sync-project-tree
|
|
cancel-in-progress: true
|
|
|
|
env:
|
|
GITEA_HOST: gitea.whitlocktech.com
|
|
DOCS_REPO: RunicGateway/docs
|
|
SELF_REPO: RunicGateway/installer
|
|
DOCS_PATH: installer/PROJECT_TREE.md
|
|
TREE_TITLE: Runic Gateway installer
|
|
ROOT_LABEL: installer
|
|
PR_BRANCH: chore/sync-installer-tree
|
|
|
|
jobs:
|
|
sync:
|
|
runs-on: ubuntu-latest
|
|
steps:
|
|
- name: Check out this repo
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 1
|
|
|
|
- name: Ensure python3 is available
|
|
run: |
|
|
set -euo pipefail
|
|
command -v python3 >/dev/null 2>&1 || { sudo apt-get update -qq && sudo apt-get install -y -qq python3; }
|
|
|
|
- name: Render PROJECT_TREE.md from tracked files
|
|
run: |
|
|
set -euo pipefail
|
|
mkdir -p _sync
|
|
{
|
|
printf '# %s — Project Tree\n\n' "${TREE_TITLE}"
|
|
printf '> **Auto-generated.** This file is maintained by the `sync-project-tree` CI workflow in\n'
|
|
printf '> the [`%s`](https://%s/%s) repository, which\n' "${SELF_REPO}" "${GITEA_HOST}" "${SELF_REPO}"
|
|
printf '> opens a pull request here whenever the tracked file layout on `main` changes. Do not edit\n'
|
|
printf '> by hand — changes will be overwritten by the next sync.\n\n'
|
|
printf 'A snapshot of the tracked files in the repository (build output, dependencies, and other\n'
|
|
printf 'git-ignored paths are excluded).\n\n'
|
|
printf '```text\n'
|
|
git ls-files | python3 .gitea/scripts/gen_tree.py "${ROOT_LABEL}"
|
|
printf '```\n'
|
|
} > _sync/PROJECT_TREE.md
|
|
echo "----- generated ${DOCS_PATH} -----"
|
|
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
|
|
env:
|
|
REGISTRY_USER: ${{ secrets.REGISTRY_USER }}
|
|
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# Secrets can carry a trailing CR/LF depending on how they were pasted;
|
|
# strip line breaks before they land in a URL or Authorization header.
|
|
CI_USER="$(printf '%s' "${REGISTRY_USER}" | tr -d '\r\n')"
|
|
CI_TOKEN="$(printf '%s' "${REGISTRY_TOKEN}" | tr -d '\r\n')"
|
|
API="https://${GITEA_HOST}/api/v1/repos/${DOCS_REPO}"
|
|
REMOTE="https://${CI_USER}:${CI_TOKEN}@${GITEA_HOST}/${DOCS_REPO}.git"
|
|
|
|
git clone --depth 1 "${REMOTE}" docs_repo
|
|
cd docs_repo
|
|
git config user.name "runic-docs-bot"
|
|
git config user.email "ci@whitlocktech.com"
|
|
|
|
mkdir -p "$(dirname "${DOCS_PATH}")"
|
|
cp ../_sync/PROJECT_TREE.md "${DOCS_PATH}"
|
|
git add "${DOCS_PATH}"
|
|
if git diff --cached --quiet; then
|
|
echo "PROJECT_TREE.md already up to date — nothing to sync."
|
|
exit 0
|
|
fi
|
|
|
|
SHORT_SHA="$(echo "${GITHUB_SHA:-local}" | cut -c1-7)"
|
|
git checkout -B "${PR_BRANCH}"
|
|
git commit -m "docs(tree): sync ${DOCS_PATH} from ${SELF_REPO}@${SHORT_SHA} [skip ci]"
|
|
git push --force "${REMOTE}" "HEAD:${PR_BRANCH}"
|
|
|
|
# Open a PR only if one isn't already open for this branch (a force-push
|
|
# to an existing open PR's head updates it in place).
|
|
OPEN="$(curl -sSf -H "Authorization: token ${CI_TOKEN}" \
|
|
"${API}/pulls?state=open&limit=50" \
|
|
| jq --arg b "${PR_BRANCH}" '[.[] | select(.head.ref == $b)] | length')"
|
|
if [ "${OPEN}" = "0" ]; then
|
|
curl -sSf -X POST "${API}/pulls" \
|
|
-H "Authorization: token ${CI_TOKEN}" \
|
|
-H "Content-Type: application/json" \
|
|
-d "$(jq -n \
|
|
--arg head "${PR_BRANCH}" \
|
|
--arg base "main" \
|
|
--arg title "docs(tree): sync ${DOCS_PATH}" \
|
|
--arg body "Automated project-tree sync from [\`${SELF_REPO}\`](https://${GITEA_HOST}/${SELF_REPO}), regenerated from tracked files on \`main\`. Merge once the layout looks right; the workflow will keep this branch current until then." \
|
|
'{head: $head, base: $base, title: $title, body: $body}')" \
|
|
>/dev/null
|
|
echo "Opened a new docs PR for ${PR_BRANCH}."
|
|
else
|
|
echo "Existing open docs PR for ${PR_BRANCH} was updated via force-push."
|
|
fi
|