From 0376eded3fcb7ff16a8ce283f3f4081bac0fd158 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Sun, 5 Jul 2026 17:21:33 +0800 Subject: [PATCH 01/18] add Crowdin --- rowdin.yml | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 rowdin.yml diff --git a/rowdin.yml b/rowdin.yml new file mode 100644 index 0000000..9722dca --- /dev/null +++ b/rowdin.yml @@ -0,0 +1,3 @@ +files: + - source: /docs/**/*.mdx + translation: /docs-crowdin-export/%locale%/%original_path%/%original_file_name%.%file_extension% From 9e07f70c78f238f1505f0002475989531d43d1c7 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Sun, 5 Jul 2026 17:24:14 +0800 Subject: [PATCH 02/18] fix crowdin file name --- rowdin.yml => сrowdin.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename rowdin.yml => сrowdin.yml (100%) diff --git a/rowdin.yml b/сrowdin.yml similarity index 100% rename from rowdin.yml rename to сrowdin.yml From f8d8c95912fea594f208079c73f959bc5f4683fc Mon Sep 17 00:00:00 2001 From: Dexoron Date: Sun, 5 Jul 2026 17:26:00 +0800 Subject: [PATCH 03/18] =?UTF-8?q?Rename=20=D1=81rowdin.yml=20to=20crowdin.?= =?UTF-8?q?yml=20(latin)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- сrowdin.yml => crowdin.yml | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename сrowdin.yml => crowdin.yml (100%) diff --git a/сrowdin.yml b/crowdin.yml similarity index 100% rename from сrowdin.yml rename to crowdin.yml From 1f3fc62cb6216b8ce1c68239c23d669c5aecec4f Mon Sep 17 00:00:00 2001 From: Dexoron Date: Sun, 5 Jul 2026 18:16:45 +0800 Subject: [PATCH 04/18] Add GitHub Actions workflow to trigger site rebuild --- .github/workflows/trigger-site-build.yml | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 .github/workflows/trigger-site-build.yml diff --git a/.github/workflows/trigger-site-build.yml b/.github/workflows/trigger-site-build.yml new file mode 100644 index 0000000..aab475a --- /dev/null +++ b/.github/workflows/trigger-site-build.yml @@ -0,0 +1,18 @@ +name: Trigger site rebuild + +on: + push: + branches: [master] + paths: + - 'docs/**' + +jobs: + trigger: + runs-on: ubuntu-latest + steps: + - name: Trigger GitLab pipeline + run: | + curl -sS -X POST \ + -F "token=${{ secrets.GITLAB_TRIGGER_TOKEN }}" \ + -F "ref=master" \ + "https://gitlab.com/api/v4/projects/83371847/trigger/pipeline" From a3f8444c2000effa28c7d25e6c61e76bcb4bd446 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Sun, 5 Jul 2026 18:24:47 +0800 Subject: [PATCH 05/18] Replace GitLab pipeline trigger with Cloudflare build --- .github/workflows/trigger-site-build.yml | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/.github/workflows/trigger-site-build.yml b/.github/workflows/trigger-site-build.yml index aab475a..cae633d 100644 --- a/.github/workflows/trigger-site-build.yml +++ b/.github/workflows/trigger-site-build.yml @@ -10,9 +10,5 @@ jobs: trigger: runs-on: ubuntu-latest steps: - - name: Trigger GitLab pipeline - run: | - curl -sS -X POST \ - -F "token=${{ secrets.GITLAB_TRIGGER_TOKEN }}" \ - -F "ref=master" \ - "https://gitlab.com/api/v4/projects/83371847/trigger/pipeline" + - name: Trigger Cloudflare Pages build + run: curl -sS -X POST "${{ secrets.CF_PAGES_DEPLOY_HOOK }}" From 05e49838f716db52f211c2a8b7e4cfd2178bfe25 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 17:58:30 +0800 Subject: [PATCH 06/18] feat: add automated repository mirroring workflows and update docs - Add GitHub Actions workflows for code and release mirroring - Remove manual sync-releases-to-gitlab.py script - Update .gitignore to track .github/ directory - Document repository mirrors and VS Code extension recommendations --- .github/workflows/code-mirror.yml | 37 ++++++ .github/workflows/release-mirror.yml | 186 ++++++++++++++++++++++++++ .gitignore | 3 +- README.md | 24 ++++ docs/ide-integration.mdx | 7 + scripts/sync-releases-to-gitlab.py | 192 --------------------------- 6 files changed, 256 insertions(+), 193 deletions(-) create mode 100644 .github/workflows/code-mirror.yml create mode 100644 .github/workflows/release-mirror.yml delete mode 100644 scripts/sync-releases-to-gitlab.py diff --git a/.github/workflows/code-mirror.yml b/.github/workflows/code-mirror.yml new file mode 100644 index 0000000..5567e08 --- /dev/null +++ b/.github/workflows/code-mirror.yml @@ -0,0 +1,37 @@ +name: Mirror source code to GitVerse + +on: + push: + branches: + - '**' + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: code-mirror-${{ github.ref }} + cancel-in-progress: true + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Mirror all branches + env: + GITVERSE_PAT: ${{ secrets.GITVERSE_PAT }} + GITVERSE_ORG: ${{ vars.GITVERSE_ORG || 'dcr_labs' }} + GITVERSE_REPO: ${{ vars.GITVERSE_REPO || 'dcr' }} + run: | + set -euo pipefail + test -n "$GITVERSE_PAT" || { echo 'GITVERSE_PAT is not configured' >&2; exit 1; } + remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git" + git remote add gitverse "$remote" + # Populate local refs first, so a manual run mirrors every branch too. + git fetch origin '+refs/heads/*:refs/heads/*' + git push --prune --force gitverse 'refs/heads/*:refs/heads/*' diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml new file mode 100644 index 0000000..80c1280 --- /dev/null +++ b/.github/workflows/release-mirror.yml @@ -0,0 +1,186 @@ +name: Mirror releases to GitVerse, SourceCraft, and GitLab + +on: + workflow_run: + workflows: + - Release + types: + - completed + workflow_dispatch: + inputs: + tag: + description: Git tag of the GitHub release to mirror (for example v0.7.1) + required: true + type: string + +permissions: + contents: read + +jobs: + mirror: + runs-on: ubuntu-latest + steps: + - name: Resolve release tag + id: release + env: + GH_TOKEN: ${{ github.token }} + MANUAL_TAG: ${{ inputs.tag }} + WORKFLOW_HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} + WORKFLOW_HEAD_SHA: ${{ github.event.workflow_run.head_sha }} + run: | + set -euo pipefail + tag="$MANUAL_TAG" + if [ -z "$tag" ] && [[ "$WORKFLOW_HEAD_BRANCH" == v* ]]; then + tag="$WORKFLOW_HEAD_BRANCH" + fi + if [ -z "$tag" ] && [ -n "$WORKFLOW_HEAD_SHA" ]; then + releases=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases?per_page=100") + while IFS= read -r candidate; do + ref=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/ref/tags/${candidate}") + sha=$(jq -r '.object.sha' <<< "$ref") + if [ "$(jq -r '.object.type' <<< "$ref")" = tag ]; then + sha=$(curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/git/tags/${sha}" | jq -r '.object.sha') + fi + if [ "$sha" = "$WORKFLOW_HEAD_SHA" ]; then + tag="$candidate" + break + fi + done < <(jq -r '.[].tag_name' <<< "$releases") + fi + if [ -z "$tag" ] || [[ "$tag" != v* ]]; then + echo 'No release tag was found; release.yml may have failed before creating a GitHub release.' + echo 'found=false' >> "$GITHUB_OUTPUT" + exit 0 + fi + echo "tag=$tag" >> "$GITHUB_OUTPUT" + echo 'found=true' >> "$GITHUB_OUTPUT" + + - name: Download GitHub release metadata and assets + if: steps.release.outputs.found == 'true' + env: + GH_TOKEN: ${{ github.token }} + TAG: ${{ steps.release.outputs.tag }} + run: | + set -euo pipefail + mkdir -p assets + curl -fsSL -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/vnd.github+json' \ + "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" > release.json + jq -e '.tag_name == $tag' --arg tag "$TAG" release.json >/dev/null + jq -r '.assets[] | [.name, .browser_download_url] | @tsv' release.json | + while IFS=$'\t' read -r name url; do + curl -fsSL -L -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/octet-stream' -o "assets/$name" "$url" + done + + - name: Sync tag and release to GitVerse + if: steps.release.outputs.found == 'true' + env: + GITVERSE_PAT: ${{ secrets.GITVERSE_PAT }} + GITVERSE_ORG: ${{ vars.GITVERSE_ORG || 'dcr_labs' }} + GITVERSE_REPO: ${{ vars.GITVERSE_REPO || 'dcr' }} + TAG: ${{ steps.release.outputs.tag }} + run: | + set -euo pipefail + test -n "$GITVERSE_PAT" || { echo 'GITVERSE_PAT is not configured' >&2; exit 1; } + project="${GITVERSE_ORG}%2F${GITVERSE_REPO}" + remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git" + git clone --mirror "https://github.com/${GITHUB_REPOSITORY}.git" mirror.git + git -C mirror.git remote add gitverse "$remote" + git -C mirror.git push gitverse "refs/tags/${TAG}:refs/tags/${TAG}" + + title=$(jq -r '.name // .tag_name' release.json) + notes=$(jq -r '.body // ""' release.json) + payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ + '{name:$title, tag_name:$tag, description:$notes, ref:$tag}') + curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "https://gitverse.ru/api/v4/projects/${project}/releases" || \ + curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}" + for file in assets/*; do + [ -f "$file" ] || continue + upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -F "file=@$file" "https://gitverse.ru/api/v4/projects/${project}/uploads") + url=$(jq -r '.url // .file_path' <<< "$upload") + name=$(basename "$file") + link=$(jq -n --arg name "$name" --arg url "$url" '{name:$name,url:$url}') + curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$link" \ + "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}/assets/links" || true + done + + - name: Sync tag and release to SourceCraft + if: steps.release.outputs.found == 'true' + env: + SOURCECRAFT_PAT: ${{ secrets.SOURCECRAFT_PAT }} + SOURCECRAFT_ORG: ${{ vars.SOURCECRAFT_ORG || 'dexoron-dev' }} + SOURCECRAFT_REPO: ${{ vars.SOURCECRAFT_REPO || 'dcr' }} + TAG: ${{ steps.release.outputs.tag }} + run: | + set -euo pipefail + test -n "$SOURCECRAFT_PAT" || { echo 'SOURCECRAFT_PAT is not configured' >&2; exit 1; } + remote="https://anyname:${SOURCECRAFT_PAT}@git.sourcecraft.dev/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}.git" + git clone "$remote" sourcecraft + git -C sourcecraft remote add github "https://github.com/${GITHUB_REPOSITORY}.git" + git -C sourcecraft fetch github "refs/tags/${TAG}:refs/tags/${TAG}" + git -C sourcecraft push origin "refs/tags/${TAG}" + + title=$(jq -r '.name // .tag_name' release.json) + notes=$(jq -r '.body // ""' release.json) + payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ + '{tag:$tag,title:$title,release_notes:$notes,publish:true}') + curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases" || \ + echo 'SourceCraft release already exists; continuing with assets.' + for file in assets/*; do + [ -f "$file" ] || continue + curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ + -F "name=$(basename "$file")" -F "file=@$file" \ + "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases/tag/${TAG}/attachments" || true + done + + - name: Sync release to GitLab + if: steps.release.outputs.found == 'true' + env: + GITLAB_PAT: ${{ secrets.GITLAB_PAT }} + GITLAB_PROJECT: ${{ vars.GITLAB_PROJECT || 'dexoron-dev/dcr-tool/dcr' }} + TAG: ${{ steps.release.outputs.tag }} + run: | + set -euo pipefail + test -n "$GITLAB_PAT" || { echo 'GITLAB_PAT is not configured' >&2; exit 1; } + project_id="${GITLAB_PROJECT//\//%2F}" + api="https://gitlab.com/api/v4/projects/${project_id}" + title=$(jq -r '.name // .tag_name' release.json) + notes=$(jq -r '.body // ""' release.json) + payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ + '{tag_name:$tag, name:$title, description:$notes}') + + # Tags are already mirrored by GitLab's built-in repository mirror. + curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "${api}/releases" || \ + curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "${api}/releases/${TAG}" + + for file in assets/*; do + [ -f "$file" ] || continue + upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + -F "file=@$file" "${api}/uploads") + url="https://gitlab.com/${GITLAB_PROJECT}$(jq -r '.url' <<< "$upload")" + name=$(basename "$file") + link=$(jq -n --arg name "$name" --arg url "$url" \ + '{name:$name, url:$url, link_type:"other"}') + curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + -H 'Content-Type: application/json' --data "$link" \ + "${api}/releases/${TAG}/assets/links" || true + done diff --git a/.gitignore b/.gitignore index d0a4f5f..8c9d461 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ sandbox/** personal/ .* -!.gitignore \ No newline at end of file +!.gitignore +!.github/ \ No newline at end of file diff --git a/README.md b/README.md index a73181c..785e694 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,30 @@ Full docs at **[dcr.dexoron.su](https://dcr.dexoron.su)** or in the [`docs/`](do --- +## Repository Mirrors + +The primary repository is hosted on GitHub. The following mirrors are available for source code and releases: + +| Region | Mirror | Contents | +|---|---|---| +| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) **(recommended)** | Source code and releases | +| Russia | [SourceCraft](https://sourcecraft.dev/dexoron-dev/dcr) | Source code and releases | +| Worldwide | [SourceForge](https://sourceforge.net/projects/dcr-tool/) **(recommended)** | Source code and releases | +| Worldwide | [GitLab](https://gitlab.com/dexoron-dev/dcr-tool/dcr) | Source code and releases | + +--- + +## VS Code Extension + +Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr) for VS Code. Do not install Microsoft C/C++ extensions for DCR projects; DCR uses `clangd` instead. If you need debugging with Microsoft's debugger, use MS C/C++ only for its debugger component. + +Recommended companion extensions: + +- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) +- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only + +--- + ## Contributors
diff --git a/docs/ide-integration.mdx b/docs/ide-integration.mdx index ba08f23..7004298 100644 --- a/docs/ide-integration.mdx +++ b/docs/ide-integration.mdx @@ -6,6 +6,13 @@ sidebar_label: IDE Integration ## VS Code +Install the recommended [DCR extension by Dexoron-Labs](https://marketplace.visualstudio.com/items?itemName=Dexoron-Labs.dcr). Do not install Microsoft C/C++ extensions for DCR projects: DCR uses `clangd`. MS C/C++ is acceptable only when you need its debugger. + +Recommended companion extensions: + +- [Even Better TOML](https://marketplace.visualstudio.com/items?itemName=tamasfe.even-better-toml) +- [CodeLLDB](https://marketplace.visualstudio.com/items?itemName=vadimcn.vscode-lldb), or MS C/C++ for debugger support only + ```bash dcr gen vscode ``` diff --git a/scripts/sync-releases-to-gitlab.py b/scripts/sync-releases-to-gitlab.py deleted file mode 100644 index d1e5bed..0000000 --- a/scripts/sync-releases-to-gitlab.py +++ /dev/null @@ -1,192 +0,0 @@ -#!/usr/bin/env python3 -"""Sync all GitHub Releases (with assets) to GitLab Releases.""" - -import json -import os -import sys -import time -import urllib.error -import urllib.request -import uuid - -GL_TOKEN = os.environ.get("GL_TOKEN") -if not GL_TOKEN: - print("FATAL: GL_TOKEN environment variable not set", file=sys.stderr) - sys.exit(1) - -GL_PROJECT = "dexoron-studio%2Fdcr" -GL_PROJECT_RAW = "dexoron-studio/dcr" -GL_API = "https://gitlab.com/api/v4" -GH_REPO = "dexoron/dcr" - - -def gl_api(method, path, data=None, filepath=None, retries=5): - url = f"{GL_API}/{path}" - headers = {"PRIVATE-TOKEN": GL_TOKEN} - - if data is not None and filepath is None: - headers["Content-Type"] = "application/json" - body = json.dumps(data).encode() - elif filepath: - boundary = uuid.uuid4().hex - with open(filepath, "rb") as f: - fdata = f.read() - fname = os.path.basename(filepath) - parts = [] - parts.append(f"--{boundary}".encode()) - parts.append(f'Content-Disposition: form-data; name="file"; filename="{fname}"'.encode()) - parts.append(b"Content-Type: application/octet-stream") - parts.append(b"") - parts.append(fdata) - parts.append(f"--{boundary}--".encode()) - parts.append(b"") - body = b"\r\n".join(parts) - headers["Content-Type"] = f"multipart/form-data; boundary={boundary}" - else: - body = None - - for attempt in range(retries): - try: - req = urllib.request.Request(url, data=body, headers=headers, method=method) - with urllib.request.urlopen(req) as resp: - text = resp.read().decode() - return json.loads(text) if text else None - except urllib.error.HTTPError as e: - body_text = e.read().decode() if e.fp else "" - if e.code in (409, 429) and attempt < retries - 1: - time.sleep(3) - continue - if attempt < retries - 1: - time.sleep(1) - continue - print(f" HTTP {e.code}: {body_text[:300]}", file=sys.stderr) - return None - except Exception as e: - if attempt < retries - 1: - time.sleep(1) - continue - print(f" Error: {e}", file=sys.stderr) - return None - return None - - -def download_file(url, dest): - with urllib.request.urlopen(url) as resp: - with open(dest, "wb") as f: - f.write(resp.read()) - - -# --- MAIN --- -print("Fetching GitHub releases...") -with urllib.request.urlopen(f"https://api.github.com/repos/{GH_REPO}/releases?per_page=50") as r: - gh_releases = json.loads(r.read().decode()) - -print(f"Found {len(gh_releases)} releases on GitHub") - -# Reverse to process oldest first (GitHub returns newest first) -gh_releases.reverse() - -print("Fetching existing GitLab releases and their assets...") -existing = gl_api("GET", f"projects/{GL_PROJECT}/releases?per_page=100") or [] -existing = existing if isinstance(existing, list) else [] - -# Build map: tag -> set of existing asset names -gl_assets = {} -for rel in existing: - tag = rel["tag_name"] - links = rel.get("assets", {}).get("links", []) or rel.get("assets", {}).get("sources", []) or [] - gl_assets[tag] = {l.get("name", "") for l in links if isinstance(l, dict)} - # Also get the release description to check if it needs updating - gl_assets[tag + "__desc"] = rel.get("description", "") - -print(f"Existing GitLab releases: {len(existing)}") - -download_dir = "/tmp/gh-assets" -os.makedirs(download_dir, exist_ok=True) - -# Reverse: process older first so newer releases with many assets get their description too -# Actually, process in original order -total = len(gh_releases) -for i, rel in enumerate(gh_releases): - tag = rel["tag_name"] - body = rel.get("body", "") or "" - assets = rel.get("assets", []) - if isinstance(assets, dict): - assets = assets.get("nodes", []) or [] - - print(f"\n{'='*60}") - print(f"[{i+1}/{total}] {tag} ({len(assets)} assets)") - - # Create or update release - existing_tag = tag in gl_assets - if not existing_tag: - print(f" Creating release...") - result = gl_api("POST", f"projects/{GL_PROJECT}/releases", data={ - "tag_name": tag, - "name": rel.get("name", tag), - "description": body, - "ref": "master", - }) - if result is None: - print(f" FAILED — skip") - continue - print(f" OK") - else: - # Update description if needed - existing_desc = gl_assets.get(tag + "__desc", "") - if body and body != existing_desc: - print(f" Updating description...") - gl_api("PUT", f"projects/{GL_PROJECT}/releases/{urllib.request.quote(tag, safe='')}", - data={"description": body}) - - existing_links = gl_assets.get(tag, set()) - - for j, asset in enumerate(assets): - aname = asset.get("name", "") - aurl = asset.get("browser_download_url", "") - if not aurl: - continue - - if aname in existing_links: - print(f" [{j+1}/{len(assets)}] {aname} — already exists") - continue - - fpath = os.path.join(download_dir, aname) - print(f" [{j+1}/{len(assets)}] {aname}...", end=" ", flush=True) - - # Download from GitHub - try: - download_file(aurl, fpath) - except Exception as e: - print(f"download failed: {e}") - continue - - # Upload to GitLab - up = gl_api("POST", f"projects/{GL_PROJECT}/uploads", filepath=fpath) - if not up or "url" not in up: - size_mb = os.path.getsize(fpath) / (1024 * 1024) - print(f"upload failed ({size_mb:.1f} MB)") - os.remove(fpath) - continue - - file_url = up["url"] - full_url = f"https://gitlab.com/{GL_PROJECT_RAW}{file_url}" - - # Link asset to release - encoded_tag = urllib.request.quote(tag, safe='') - link = gl_api("POST", - f"projects/{GL_PROJECT}/releases/{encoded_tag}/assets/links", - data={"name": aname, "url": full_url}, - retries=5) - if link: - print(f"linked") - existing_links.add(aname) - else: - print(f"link failed") - - os.remove(fpath) - - time.sleep(0.3) - -print(f"\n{'='*60}") -print("Done!") \ No newline at end of file From f7353f9d0369d77019fd13dee9f6347320f55ce7 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 18:03:20 +0800 Subject: [PATCH 07/18] fix git sync --- .github/workflows/code-mirror.yml | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/workflows/code-mirror.yml b/.github/workflows/code-mirror.yml index 5567e08..a54e615 100644 --- a/.github/workflows/code-mirror.yml +++ b/.github/workflows/code-mirror.yml @@ -32,6 +32,7 @@ jobs: test -n "$GITVERSE_PAT" || { echo 'GITVERSE_PAT is not configured' >&2; exit 1; } remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git" git remote add gitverse "$remote" - # Populate local refs first, so a manual run mirrors every branch too. - git fetch origin '+refs/heads/*:refs/heads/*' - git push --prune --force gitverse 'refs/heads/*:refs/heads/*' + # Use a private ref namespace: Git will not overwrite the checked-out + # branch, and broken legacy submodule metadata is not traversed. + git fetch --no-recurse-submodules origin '+refs/heads/*:refs/mirror/*' + git push --prune --force gitverse 'refs/mirror/*:refs/heads/*' From 5eb00037ea75c8b3f69d0b6d54b4903f3d5d2b66 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 19:14:13 +0800 Subject: [PATCH 08/18] fix release sync --- .github/workflows/release-mirror.yml | 62 ++++++++++++++++------------ 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index 80c1280..c7ff524 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -81,6 +81,7 @@ jobs: - name: Sync tag and release to GitVerse if: steps.release.outputs.found == 'true' + continue-on-error: true env: GITVERSE_PAT: ${{ secrets.GITVERSE_PAT }} GITVERSE_ORG: ${{ vars.GITVERSE_ORG || 'dcr_labs' }} @@ -89,36 +90,42 @@ jobs: run: | set -euo pipefail test -n "$GITVERSE_PAT" || { echo 'GITVERSE_PAT is not configured' >&2; exit 1; } - project="${GITVERSE_ORG}%2F${GITVERSE_REPO}" - remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git" - git clone --mirror "https://github.com/${GITHUB_REPOSITORY}.git" mirror.git - git -C mirror.git remote add gitverse "$remote" - git -C mirror.git push gitverse "refs/tags/${TAG}:refs/tags/${TAG}" + api="https://api.gitverse.ru" + accept='Accept: application/vnd.gitverse.object+json;version=1' + remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git" + git clone --mirror "https://github.com/${GITHUB_REPOSITORY}.git" mirror.git + git -C mirror.git remote add gitverse "$remote" + git -C mirror.git push gitverse "refs/tags/${TAG}:refs/tags/${TAG}" + + title=$(jq -r '.name // .tag_name' release.json) + notes=$(jq -r '.body // ""' release.json) + payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ + '{name:$title, tag_name:$tag, body:$notes, draft:false, prerelease:false}') + created=0 + if [ "$(curl -sS -o /dev/null -w '%{http_code}' -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" 2>/dev/null)" = '404' ]; then + curl -fsSL -X POST -H "$accept" -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases" > gitverse-release.json + created=1 + else + curl -fsSL -H "$accept" -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" > gitverse-release.json + fi + for file in assets/*; do + [ -f "$file" ] || continue + # Upload replaces an existing asset with the same name. + curl -fsSL -X POST -H "$accept" -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -F "name=$(basename "$file")" -F "attachment=@$file" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/$(jq -r '.id' gitverse-release.json)/assets" \ + >/dev/null + done + echo "gitverse=ok" >> "$GITHUB_OUTPUT" - title=$(jq -r '.name // .tag_name' release.json) - notes=$(jq -r '.body // ""' release.json) - payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ - '{name:$title, tag_name:$tag, description:$notes, ref:$tag}') - curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "https://gitverse.ru/api/v4/projects/${project}/releases" || \ - curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}" - for file in assets/*; do - [ -f "$file" ] || continue - upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -F "file=@$file" "https://gitverse.ru/api/v4/projects/${project}/uploads") - url=$(jq -r '.url // .file_path' <<< "$upload") - name=$(basename "$file") - link=$(jq -n --arg name "$name" --arg url "$url" '{name:$name,url:$url}') - curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -H 'Content-Type: application/json' --data "$link" \ - "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}/assets/links" || true - done - name: Sync tag and release to SourceCraft if: steps.release.outputs.found == 'true' + continue-on-error: true env: SOURCECRAFT_PAT: ${{ secrets.SOURCECRAFT_PAT }} SOURCECRAFT_ORG: ${{ vars.SOURCECRAFT_ORG || 'dexoron-dev' }} @@ -147,9 +154,11 @@ jobs: -F "name=$(basename "$file")" -F "file=@$file" \ "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases/tag/${TAG}/attachments" || true done + echo "sourcecraft=ok" >> "$GITHUB_OUTPUT" - name: Sync release to GitLab if: steps.release.outputs.found == 'true' + continue-on-error: true env: GITLAB_PAT: ${{ secrets.GITLAB_PAT }} GITLAB_PROJECT: ${{ vars.GITLAB_PROJECT || 'dexoron-dev/dcr-tool/dcr' }} @@ -184,3 +193,4 @@ jobs: -H 'Content-Type: application/json' --data "$link" \ "${api}/releases/${TAG}/assets/links" || true done + echo "gitlab=ok" >> "$GITHUB_OUTPUT" From 9add0698794ae2741b6fe461711104b979c17e5d Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 19:24:31 +0800 Subject: [PATCH 09/18] fix release sync --- .github/workflows/release-mirror.yml | 41 ++++++++++++++-------------- 1 file changed, 21 insertions(+), 20 deletions(-) diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index c7ff524..93948f7 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -101,26 +101,27 @@ jobs: notes=$(jq -r '.body // ""' release.json) payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ '{name:$title, tag_name:$tag, body:$notes, draft:false, prerelease:false}') - created=0 - if [ "$(curl -sS -o /dev/null -w '%{http_code}' -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" 2>/dev/null)" = '404' ]; then - curl -fsSL -X POST -H "$accept" -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases" > gitverse-release.json - created=1 - else - curl -fsSL -H "$accept" -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" > gitverse-release.json - fi - for file in assets/*; do - [ -f "$file" ] || continue - # Upload replaces an existing asset with the same name. - curl -fsSL -X POST -H "$accept" -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -F "name=$(basename "$file")" -F "attachment=@$file" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/$(jq -r '.id' gitverse-release.json)/assets" \ - >/dev/null - done - echo "gitverse=ok" >> "$GITHUB_OUTPUT" + created=0 + if [ "$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $GITVERSE_PAT" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" 2>/dev/null)" = '404' ]; then + curl -fsSL -X POST -H "$accept" -H "Authorization: Bearer $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases" > gitverse-release.json + created=1 + else + curl -fsSL -H "$accept" -H "Authorization: Bearer $GITVERSE_PAT" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" > gitverse-release.json + fi + for file in assets/*; do + [ -f "$file" ] || continue + # Upload replaces an existing asset with the same name. + curl -fsSL -X POST -H "$accept" -H "Authorization: Bearer $GITVERSE_PAT" \ + -F "name=$(basename "$file")" -F "attachment=@$file" \ + "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/$(jq -r '.id' gitverse-release.json)/assets" \ + >/dev/null + done + echo "gitverse=ok" >> "$GITHUB_OUTPUT" + - name: Sync tag and release to SourceCraft From 9e2b7a897a368c30af11342e350c4363ffd6bbf0 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 19:45:38 +0800 Subject: [PATCH 10/18] fix release sync --- .github/workflows/release-mirror.yml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index 93948f7..e039475 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -145,16 +145,22 @@ jobs: notes=$(jq -r '.body // ""' release.json) payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ '{tag:$tag,title:$title,release_notes:$notes,publish:true}') + + # Force update release + attach all assets curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ -H 'Content-Type: application/json' --data "$payload" \ "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases" || \ - echo 'SourceCraft release already exists; continuing with assets.' + curl -fsSL -X PUT -H "Authorization: Bearer $SOURCECRAFT_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases/${TAG}" + for file in assets/*; do [ -f "$file" ] || continue curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ -F "name=$(basename "$file")" -F "file=@$file" \ "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases/tag/${TAG}/attachments" || true done + echo "sourcecraft=ok" >> "$GITHUB_OUTPUT" - name: Sync release to GitLab From b6153dbaac46882e3ee12d206257c110917ffbe8 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 20:13:30 +0800 Subject: [PATCH 11/18] fix release sync --- .github/workflows/release-mirror.yml | 209 +++++++++++++++++++-------- 1 file changed, 145 insertions(+), 64 deletions(-) diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index e039475..c487816 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -17,8 +17,11 @@ permissions: contents: read jobs: - mirror: + setup: runs-on: ubuntu-latest + outputs: + tag: ${{ steps.release.outputs.tag }} + found: ${{ steps.release.outputs.found }} steps: - name: Resolve release tag id: release @@ -73,65 +76,113 @@ jobs: -H 'Accept: application/vnd.github+json' \ "https://api.github.com/repos/${GITHUB_REPOSITORY}/releases/tags/${TAG}" > release.json jq -e '.tag_name == $tag' --arg tag "$TAG" release.json >/dev/null - jq -r '.assets[] | [.name, .browser_download_url] | @tsv' release.json | - while IFS=$'\t' read -r name url; do - curl -fsSL -L -H "Authorization: Bearer $GH_TOKEN" \ - -H 'Accept: application/octet-stream' -o "assets/$name" "$url" - done + + total=$(jq '.assets | length' release.json) + downloaded=0 + failed=0 + while IFS=$' ' read -r name url; do + if curl -fsSL -L -H "Authorization: Bearer $GH_TOKEN" \ + -H 'Accept: application/octet-stream' -o "assets/$name" "$url"; then + downloaded=$((downloaded+1)) + else + echo "::warning::Failed to download asset: $name" + failed=$((failed+1)) + fi + done < <(jq -r '.assets[] | [.name, .browser_download_url] | @tsv' release.json) + + echo "Downloaded ${downloaded}/${total} assets (${failed} failed)" + [ "$failed" -eq 0 ] || { echo 'Not all release assets were downloaded.' >&2; exit 1; } + + - name: Upload release metadata artifact + if: steps.release.outputs.found == 'true' + uses: actions/upload-artifact@v4 + with: + name: release-mirror-data + path: | + release.json + assets/* + if-no-files-found: error + + mirror-gitverse: + needs: setup + if: needs.setup.outputs.found == 'true' + runs-on: ubuntu-latest + steps: + - name: Download release metadata + uses: actions/download-artifact@v4 + with: + name: release-mirror-data - name: Sync tag and release to GitVerse - if: steps.release.outputs.found == 'true' - continue-on-error: true env: GITVERSE_PAT: ${{ secrets.GITVERSE_PAT }} GITVERSE_ORG: ${{ vars.GITVERSE_ORG || 'dcr_labs' }} GITVERSE_REPO: ${{ vars.GITVERSE_REPO || 'dcr' }} - TAG: ${{ steps.release.outputs.tag }} + TAG: ${{ needs.setup.outputs.tag }} run: | set -euo pipefail test -n "$GITVERSE_PAT" || { echo 'GITVERSE_PAT is not configured' >&2; exit 1; } - api="https://api.gitverse.ru" - accept='Accept: application/vnd.gitverse.object+json;version=1' - remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git" - git clone --mirror "https://github.com/${GITHUB_REPOSITORY}.git" mirror.git - git -C mirror.git remote add gitverse "$remote" - git -C mirror.git push gitverse "refs/tags/${TAG}:refs/tags/${TAG}" + project="${GITVERSE_ORG}%2F${GITVERSE_REPO}" + remote="https://oauth2:${GITVERSE_PAT}@gitverse.ru/${GITVERSE_ORG}/${GITVERSE_REPO}.git" + git clone --mirror "https://github.com/${GITHUB_REPOSITORY}.git" mirror.git + git -C mirror.git remote add gitverse "$remote" + git -C mirror.git push gitverse "refs/tags/${TAG}:refs/tags/${TAG}" - title=$(jq -r '.name // .tag_name' release.json) - notes=$(jq -r '.body // ""' release.json) - payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ - '{name:$title, tag_name:$tag, body:$notes, draft:false, prerelease:false}') - created=0 - if [ "$(curl -sS -o /dev/null -w '%{http_code}' -H "Authorization: Bearer $GITVERSE_PAT" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" 2>/dev/null)" = '404' ]; then - curl -fsSL -X POST -H "$accept" -H "Authorization: Bearer $GITVERSE_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases" > gitverse-release.json - created=1 + title=$(jq -r '.name // .tag_name' release.json) + notes=$(jq -r '.body // ""' release.json) + payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ + '{name:$title, tag_name:$tag, description:$notes, ref:$tag}') + + http=$(curl -sS -o gitverse-release.json -w '%{http_code}' \ + -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}" || true) + if [ "$http" = '200' ]; then + echo "GitVerse release ${TAG} already exists; skipping create." + else + curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "https://gitverse.ru/api/v4/projects/${project}/releases" || \ + curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$payload" \ + "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}" + fi + + uploaded=0; failed=0 + for file in assets/*; do + [ -f "$file" ] || continue + upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -F "file=@$file" "https://gitverse.ru/api/v4/projects/${project}/uploads") + url=$(jq -r '.url // .file_path' <<< "$upload") + name=$(basename "$file") + link=$(jq -n --arg name "$name" --arg url "$url" '{name:$name,url:$url}') + if curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$link" \ + "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}/assets/links" >/dev/null 2>&1; then + uploaded=$((uploaded+1)) else - curl -fsSL -H "$accept" -H "Authorization: Bearer $GITVERSE_PAT" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/tags/${TAG}" > gitverse-release.json + echo "::warning::GitVerse: failed to attach asset $name" + failed=$((failed+1)) fi - for file in assets/*; do - [ -f "$file" ] || continue - # Upload replaces an existing asset with the same name. - curl -fsSL -X POST -H "$accept" -H "Authorization: Bearer $GITVERSE_PAT" \ - -F "name=$(basename "$file")" -F "attachment=@$file" \ - "${api}/repos/${GITVERSE_ORG}/${GITVERSE_REPO}/releases/$(jq -r '.id' gitverse-release.json)/assets" \ - >/dev/null - done - echo "gitverse=ok" >> "$GITHUB_OUTPUT" - + done + echo "GitVerse: uploaded ${uploaded}, failed ${failed}" + [ "$failed" -eq 0 ] || exit 1 + mirror-sourcecraft: + needs: setup + if: needs.setup.outputs.found == 'true' + runs-on: ubuntu-latest + steps: + - name: Download release metadata + uses: actions/download-artifact@v4 + with: + name: release-mirror-data - name: Sync tag and release to SourceCraft - if: steps.release.outputs.found == 'true' - continue-on-error: true env: SOURCECRAFT_PAT: ${{ secrets.SOURCECRAFT_PAT }} SOURCECRAFT_ORG: ${{ vars.SOURCECRAFT_ORG || 'dexoron-dev' }} SOURCECRAFT_REPO: ${{ vars.SOURCECRAFT_REPO || 'dcr' }} - TAG: ${{ steps.release.outputs.tag }} + TAG: ${{ needs.setup.outputs.tag }} run: | set -euo pipefail test -n "$SOURCECRAFT_PAT" || { echo 'SOURCECRAFT_PAT is not configured' >&2; exit 1; } @@ -145,31 +196,51 @@ jobs: notes=$(jq -r '.body // ""' release.json) payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ '{tag:$tag,title:$title,release_notes:$notes,publish:true}') + base="https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}" - # Force update release + attach all assets - curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases" || \ - curl -fsSL -X PUT -H "Authorization: Bearer $SOURCECRAFT_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases/${TAG}" + http=$(curl -sS -o /dev/null -w '%{http_code}' \ + -H "Authorization: Bearer $SOURCECRAFT_PAT" "${base}/releases/tag/${TAG}" || true) + if [ "$http" != '200' ]; then + # Try create, then update (idempotent). + curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ + -H 'Content-Type: application/json' --data "$payload" "${base}/releases" || \ + curl -fsSL -X PUT -H "Authorization: Bearer $SOURCECRAFT_PAT" \ + -H 'Content-Type: application/json' --data "$payload" "${base}/releases/${TAG}" || \ + echo 'SourceCraft release already exists; continuing with assets.' + else + echo "SourceCraft release ${TAG} already exists; continuing with assets." + fi + uploaded=0; failed=0 for file in assets/*; do [ -f "$file" ] || continue - curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ + if curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ -F "name=$(basename "$file")" -F "file=@$file" \ - "https://api.sourcecraft.tech/repos/${SOURCECRAFT_ORG}/${SOURCECRAFT_REPO}/releases/tag/${TAG}/attachments" || true + "${base}/releases/tag/${TAG}/attachments" >/dev/null 2>&1; then + uploaded=$((uploaded+1)) + else + echo "::warning::SourceCraft: failed to attach asset $(basename "$file")" + failed=$((failed+1)) + fi done + echo "SourceCraft: uploaded ${uploaded}, failed ${failed}" + [ "$failed" -eq 0 ] || exit 1 - echo "sourcecraft=ok" >> "$GITHUB_OUTPUT" + mirror-gitlab: + needs: setup + if: needs.setup.outputs.found == 'true' + runs-on: ubuntu-latest + steps: + - name: Download release metadata + uses: actions/download-artifact@v4 + with: + name: release-mirror-data - name: Sync release to GitLab - if: steps.release.outputs.found == 'true' - continue-on-error: true env: GITLAB_PAT: ${{ secrets.GITLAB_PAT }} GITLAB_PROJECT: ${{ vars.GITLAB_PROJECT || 'dexoron-dev/dcr-tool/dcr' }} - TAG: ${{ steps.release.outputs.tag }} + TAG: ${{ needs.setup.outputs.tag }} run: | set -euo pipefail test -n "$GITLAB_PAT" || { echo 'GITLAB_PAT is not configured' >&2; exit 1; } @@ -180,14 +251,18 @@ jobs: payload=$(jq -n --arg tag "$TAG" --arg title "$title" --arg notes "$notes" \ '{tag_name:$tag, name:$title, description:$notes}') - # Tags are already mirrored by GitLab's built-in repository mirror. - curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "${api}/releases" || \ - curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITLAB_PAT" \ - -H 'Content-Type: application/json' --data "$payload" \ - "${api}/releases/${TAG}" + http=$(curl -sS -o /dev/null -w '%{http_code}' \ + -H "PRIVATE-TOKEN: $GITLAB_PAT" "${api}/releases/${TAG}" || true) + if [ "$http" != '200' ]; then + curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + -H 'Content-Type: application/json' --data "$payload" "${api}/releases" || \ + curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + -H 'Content-Type: application/json' --data "$payload" "${api}/releases/${TAG}" + else + echo "GitLab release ${TAG} already exists; continuing with assets." + fi + uploaded=0; failed=0 for file in assets/*; do [ -f "$file" ] || continue upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ @@ -196,8 +271,14 @@ jobs: name=$(basename "$file") link=$(jq -n --arg name "$name" --arg url "$url" \ '{name:$name, url:$url, link_type:"other"}') - curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + if curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ -H 'Content-Type: application/json' --data "$link" \ - "${api}/releases/${TAG}/assets/links" || true + "${api}/releases/${TAG}/assets/links" >/dev/null 2>&1; then + uploaded=$((uploaded+1)) + else + echo "::warning::GitLab: failed to attach asset $name" + failed=$((failed+1)) + fi done - echo "gitlab=ok" >> "$GITHUB_OUTPUT" + echo "GitLab: uploaded ${uploaded}, failed ${failed}" + [ "$failed" -eq 0 ] || exit 1 \ No newline at end of file From c6af6c51966b5ebe9c658ccb2b4fa757d484b4a7 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 20:25:36 +0800 Subject: [PATCH 12/18] fix release sync --- .github/workflows/ci.yml | 14 ++++++++- .github/workflows/release-mirror.yml | 47 +++++++++++++++++++++------- 2 files changed, 48 insertions(+), 13 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1f7bcea..01c1226 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,7 +6,19 @@ on: - master - main - dev + paths: + - 'src/**' + - 'tests/**' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ci.yml' pull_request: + paths: + - 'src/**' + - 'tests/**' + - 'Cargo.toml' + - 'Cargo.lock' + - '.github/workflows/ci.yml' env: CARGO_TERM_COLOR: always @@ -151,4 +163,4 @@ jobs: - name: sccache stats if: always() - run: sccache --show-stats || true + run: sccache --show-stats || true \ No newline at end of file diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index c487816..d1ceb29 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -150,18 +150,41 @@ jobs: uploaded=0; failed=0 for file in assets/*; do [ -f "$file" ] || continue - upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -F "file=@$file" "https://gitverse.ru/api/v4/projects/${project}/uploads") - url=$(jq -r '.url // .file_path' <<< "$upload") name=$(basename "$file") - link=$(jq -n --arg name "$name" --arg url "$url" '{name:$name,url:$url}') - if curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ - -H 'Content-Type: application/json' --data "$link" \ - "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}/assets/links" >/dev/null 2>&1; then - uploaded=$((uploaded+1)) - else - echo "::warning::GitVerse: failed to attach asset $name" + + # Step 1: Upload file and capture response with HTTP code + upload_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -F "file=@$file" "https://gitverse.ru/api/v4/projects/${project}/uploads") + http_code=$(echo "$upload_response" | tail -n1) + body=$(echo "$upload_response" | sed '$d') + + if [ "$http_code" != "201" ] && [ "$http_code" != "200" ]; then + echo "::warning::GitVerse: failed to upload $name (HTTP $http_code): $body" failed=$((failed+1)) + continue + fi + + # Extract file path from upload response + url=$(jq -r '.url // .file_path' <<< "$body" 2>/dev/null || echo "") + if [ -z "$url" ]; then + echo "::warning::GitVerse: failed to parse upload response for $name: $body" + failed=$((failed+1)) + continue + fi + + # Step 2: Create asset link in release + link=$(jq -n --arg name "$name" --arg url "$url" '{name:$name,url:$url}') + link_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ + -H 'Content-Type: application/json' --data "$link" \ + "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}/assets/links") + link_http=$(echo "$link_response" | tail -n1) + link_body=$(echo "$link_response" | sed '$d') + + if [ "$link_http" != "201" ] && [ "$link_http" != "200" ]; then + echo "::warning::GitVerse: failed to attach asset $name (HTTP $link_http): $link_body" + failed=$((failed+1)) + else + uploaded=$((uploaded+1)) fi done echo "GitVerse: uploaded ${uploaded}, failed ${failed}" @@ -215,8 +238,8 @@ jobs: for file in assets/*; do [ -f "$file" ] || continue if curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ - -F "name=$(basename "$file")" -F "file=@$file" \ - "${base}/releases/tag/${TAG}/attachments" >/dev/null 2>&1; then + -F "file=@$file" \ + "${base}/releases/tag/${TAG}/assets" >/dev/null 2>&1; then uploaded=$((uploaded+1)) else echo "::warning::SourceCraft: failed to attach asset $(basename "$file")" From dd89109f92af83e4fa75148f2caf2005a6d466fa Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 20:30:45 +0800 Subject: [PATCH 13/18] fix release sync --- .github/workflows/release-mirror.yml | 53 ++++++++++++++++++++-------- 1 file changed, 39 insertions(+), 14 deletions(-) diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index d1ceb29..e07211e 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -180,11 +180,15 @@ jobs: link_http=$(echo "$link_response" | tail -n1) link_body=$(echo "$link_response" | sed '$d') - if [ "$link_http" != "201" ] && [ "$link_http" != "200" ]; then + # Accept 201 (created), 200 (ok), or 409 (already exists) + if [ "$link_http" = "201" ] || [ "$link_http" = "200" ]; then + uploaded=$((uploaded+1)) + elif [ "$link_http" = "409" ]; then + echo "GitVerse: asset $name already exists (skipping)" + uploaded=$((uploaded+1)) + else echo "::warning::GitVerse: failed to attach asset $name (HTTP $link_http): $link_body" failed=$((failed+1)) - else - uploaded=$((uploaded+1)) fi done echo "GitVerse: uploaded ${uploaded}, failed ${failed}" @@ -234,19 +238,30 @@ jobs: echo "SourceCraft release ${TAG} already exists; continuing with assets." fi - uploaded=0; failed=0 + uploaded=0; failed=0; skipped=0 for file in assets/*; do [ -f "$file" ] || continue - if curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ + name=$(basename "$file") + + # Try to upload asset with error handling + upload_response=$(curl -sS -w "\n%{http_code}" -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ -F "file=@$file" \ - "${base}/releases/tag/${TAG}/assets" >/dev/null 2>&1; then + "${base}/releases/tag/${TAG}/assets") + http_code=$(echo "$upload_response" | tail -n1) + body=$(echo "$upload_response" | sed '$d') + + # Accept 201 (created), 200 (ok), 409 (conflict - already exists) + if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then uploaded=$((uploaded+1)) + elif [ "$http_code" = "409" ]; then + echo "SourceCraft: asset $name already exists (skipping)" + skipped=$((skipped+1)) else - echo "::warning::SourceCraft: failed to attach asset $(basename "$file")" + echo "::warning::SourceCraft: failed to attach asset $name (HTTP $http_code)" failed=$((failed+1)) fi done - echo "SourceCraft: uploaded ${uploaded}, failed ${failed}" + echo "SourceCraft: uploaded ${uploaded}, skipped ${skipped}, failed ${failed}" [ "$failed" -eq 0 ] || exit 1 mirror-gitlab: @@ -285,23 +300,33 @@ jobs: echo "GitLab release ${TAG} already exists; continuing with assets." fi - uploaded=0; failed=0 + uploaded=0; failed=0; skipped=0 for file in assets/*; do [ -f "$file" ] || continue + name=$(basename "$file") + upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ -F "file=@$file" "${api}/uploads") url="https://gitlab.com/${GITLAB_PROJECT}$(jq -r '.url' <<< "$upload")" - name=$(basename "$file") link=$(jq -n --arg name "$name" --arg url "$url" \ '{name:$name, url:$url, link_type:"other"}') - if curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + + link_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ -H 'Content-Type: application/json' --data "$link" \ - "${api}/releases/${TAG}/assets/links" >/dev/null 2>&1; then + "${api}/releases/${TAG}/assets/links") + link_http=$(echo "$link_response" | tail -n1) + link_body=$(echo "$link_response" | sed '$d') + + # Accept 201 (created), 200 (ok), 409 (already exists) + if [ "$link_http" = "201" ] || [ "$link_http" = "200" ]; then uploaded=$((uploaded+1)) + elif [ "$link_http" = "409" ]; then + echo "GitLab: asset $name already exists (skipping)" + skipped=$((skipped+1)) else - echo "::warning::GitLab: failed to attach asset $name" + echo "::warning::GitLab: failed to attach asset $name (HTTP $link_http)" failed=$((failed+1)) fi done - echo "GitLab: uploaded ${uploaded}, failed ${failed}" + echo "GitLab: uploaded ${uploaded}, skipped ${skipped}, failed ${failed}" [ "$failed" -eq 0 ] || exit 1 \ No newline at end of file From b6ecb2aa79fc2a71a9fafd9d33d828a1e28367d8 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 20:48:12 +0800 Subject: [PATCH 14/18] fix release sync --- .github/workflows/release-mirror.yml | 76 +++++++--------------------- 1 file changed, 17 insertions(+), 59 deletions(-) diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index e07211e..322b513 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -152,42 +152,34 @@ jobs: [ -f "$file" ] || continue name=$(basename "$file") - # Step 1: Upload file and capture response with HTTP code upload_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ -F "file=@$file" "https://gitverse.ru/api/v4/projects/${project}/uploads") http_code=$(echo "$upload_response" | tail -n1) body=$(echo "$upload_response" | sed '$d') if [ "$http_code" != "201" ] && [ "$http_code" != "200" ]; then - echo "::warning::GitVerse: failed to upload $name (HTTP $http_code): $body" + echo "::warning::GitVerse: failed to upload $name (HTTP $http_code)" failed=$((failed+1)) continue fi - # Extract file path from upload response url=$(jq -r '.url // .file_path' <<< "$body" 2>/dev/null || echo "") if [ -z "$url" ]; then - echo "::warning::GitVerse: failed to parse upload response for $name: $body" + echo "::warning::GitVerse: failed to parse upload response for $name" failed=$((failed+1)) continue fi - # Step 2: Create asset link in release link=$(jq -n --arg name "$name" --arg url "$url" '{name:$name,url:$url}') link_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITVERSE_PAT" \ -H 'Content-Type: application/json' --data "$link" \ "https://gitverse.ru/api/v4/projects/${project}/releases/${TAG}/assets/links") link_http=$(echo "$link_response" | tail -n1) - link_body=$(echo "$link_response" | sed '$d') - # Accept 201 (created), 200 (ok), or 409 (already exists) - if [ "$link_http" = "201" ] || [ "$link_http" = "200" ]; then - uploaded=$((uploaded+1)) - elif [ "$link_http" = "409" ]; then - echo "GitVerse: asset $name already exists (skipping)" + if [ "$link_http" = "201" ] || [ "$link_http" = "200" ] || [ "$link_http" = "409" ]; then uploaded=$((uploaded+1)) else - echo "::warning::GitVerse: failed to attach asset $name (HTTP $link_http): $link_body" + echo "::warning::GitVerse: failed to attach asset $name (HTTP $link_http)" failed=$((failed+1)) fi done @@ -228,7 +220,6 @@ jobs: http=$(curl -sS -o /dev/null -w '%{http_code}' \ -H "Authorization: Bearer $SOURCECRAFT_PAT" "${base}/releases/tag/${TAG}" || true) if [ "$http" != '200' ]; then - # Try create, then update (idempotent). curl -fsSL -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ -H 'Content-Type: application/json' --data "$payload" "${base}/releases" || \ curl -fsSL -X PUT -H "Authorization: Bearer $SOURCECRAFT_PAT" \ @@ -238,36 +229,33 @@ jobs: echo "SourceCraft release ${TAG} already exists; continuing with assets." fi - uploaded=0; failed=0; skipped=0 + uploaded=0; failed=0 for file in assets/*; do [ -f "$file" ] || continue name=$(basename "$file") - # Try to upload asset with error handling upload_response=$(curl -sS -w "\n%{http_code}" -X POST -H "Authorization: Bearer $SOURCECRAFT_PAT" \ -F "file=@$file" \ "${base}/releases/tag/${TAG}/assets") http_code=$(echo "$upload_response" | tail -n1) - body=$(echo "$upload_response" | sed '$d') - # Accept 201 (created), 200 (ok), 409 (conflict - already exists) if [ "$http_code" = "201" ] || [ "$http_code" = "200" ]; then uploaded=$((uploaded+1)) elif [ "$http_code" = "409" ]; then - echo "SourceCraft: asset $name already exists (skipping)" - skipped=$((skipped+1)) + uploaded=$((uploaded+1)) else echo "::warning::SourceCraft: failed to attach asset $name (HTTP $http_code)" failed=$((failed+1)) fi done - echo "SourceCraft: uploaded ${uploaded}, skipped ${skipped}, failed ${failed}" + echo "SourceCraft: uploaded ${uploaded}, failed ${failed}" [ "$failed" -eq 0 ] || exit 1 mirror-gitlab: needs: setup if: needs.setup.outputs.found == 'true' runs-on: ubuntu-latest + continue-on-error: true steps: - name: Download release metadata uses: actions/download-artifact@v4 @@ -281,7 +269,8 @@ jobs: TAG: ${{ needs.setup.outputs.tag }} run: | set -euo pipefail - test -n "$GITLAB_PAT" || { echo 'GITLAB_PAT is not configured' >&2; exit 1; } + test -n "$GITLAB_PAT" || { echo 'GITLAB_PAT is not configured' >&2; exit 0; } + project_id="${GITLAB_PROJECT//\//%2F}" api="https://gitlab.com/api/v4/projects/${project_id}" title=$(jq -r '.name // .tag_name' release.json) @@ -291,42 +280,11 @@ jobs: http=$(curl -sS -o /dev/null -w '%{http_code}' \ -H "PRIVATE-TOKEN: $GITLAB_PAT" "${api}/releases/${TAG}" || true) - if [ "$http" != '200' ]; then - curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ - -H 'Content-Type: application/json' --data "$payload" "${api}/releases" || \ - curl -fsSL -X PUT -H "PRIVATE-TOKEN: $GITLAB_PAT" \ - -H 'Content-Type: application/json' --data "$payload" "${api}/releases/${TAG}" - else - echo "GitLab release ${TAG} already exists; continuing with assets." + if [ "$http" = '200' ]; then + echo "GitLab release ${TAG} already exists; skipping" + exit 0 fi - - uploaded=0; failed=0; skipped=0 - for file in assets/*; do - [ -f "$file" ] || continue - name=$(basename "$file") - - upload=$(curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ - -F "file=@$file" "${api}/uploads") - url="https://gitlab.com/${GITLAB_PROJECT}$(jq -r '.url' <<< "$upload")" - link=$(jq -n --arg name "$name" --arg url "$url" \ - '{name:$name, url:$url, link_type:"other"}') - - link_response=$(curl -sS -w "\n%{http_code}" -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ - -H 'Content-Type: application/json' --data "$link" \ - "${api}/releases/${TAG}/assets/links") - link_http=$(echo "$link_response" | tail -n1) - link_body=$(echo "$link_response" | sed '$d') - - # Accept 201 (created), 200 (ok), 409 (already exists) - if [ "$link_http" = "201" ] || [ "$link_http" = "200" ]; then - uploaded=$((uploaded+1)) - elif [ "$link_http" = "409" ]; then - echo "GitLab: asset $name already exists (skipping)" - skipped=$((skipped+1)) - else - echo "::warning::GitLab: failed to attach asset $name (HTTP $link_http)" - failed=$((failed+1)) - fi - done - echo "GitLab: uploaded ${uploaded}, skipped ${skipped}, failed ${failed}" - [ "$failed" -eq 0 ] || exit 1 \ No newline at end of file + + echo "Creating release on GitLab..." + curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ + -H 'Content-Type: application/json' --data "$payload" "${api}/releases" || true \ No newline at end of file From 8609d1217969403dc0773a2fcb2c4aaa336c8d4a Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 20:49:36 +0800 Subject: [PATCH 15/18] fix release sync --- .github/workflows/release-mirror.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release-mirror.yml b/.github/workflows/release-mirror.yml index 322b513..172bc9f 100644 --- a/.github/workflows/release-mirror.yml +++ b/.github/workflows/release-mirror.yml @@ -270,7 +270,7 @@ jobs: run: | set -euo pipefail test -n "$GITLAB_PAT" || { echo 'GITLAB_PAT is not configured' >&2; exit 0; } - + project_id="${GITLAB_PROJECT//\//%2F}" api="https://gitlab.com/api/v4/projects/${project_id}" title=$(jq -r '.name // .tag_name' release.json) @@ -284,7 +284,7 @@ jobs: echo "GitLab release ${TAG} already exists; skipping" exit 0 fi - + echo "Creating release on GitLab..." curl -fsSL -X POST -H "PRIVATE-TOKEN: $GITLAB_PAT" \ - -H 'Content-Type: application/json' --data "$payload" "${api}/releases" || true \ No newline at end of file + -H 'Content-Type: application/json' --data "$payload" "${api}/releases" || true From 9d21294cb4878c4103f9364d1617cf65b9fc5d43 Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 21:27:21 +0800 Subject: [PATCH 16/18] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 785e694..ca86bf0 100644 --- a/README.md +++ b/README.md @@ -222,8 +222,8 @@ The primary repository is hosted on GitHub. The following mirrors are available | Region | Mirror | Contents | |---|---|---| -| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) **(recommended)** | Source code and releases | -| Russia | [SourceCraft](https://sourcecraft.dev/dexoron-dev/dcr) | Source code and releases | +| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) **(recommended)** | Source code | +| Russia | [SourceCraft](https://sourcecraft.dev/dexoron-dev/dcr) | Source code | | Worldwide | [SourceForge](https://sourceforge.net/projects/dcr-tool/) **(recommended)** | Source code and releases | | Worldwide | [GitLab](https://gitlab.com/dexoron-dev/dcr-tool/dcr) | Source code and releases | From f3605a86a22c0ecfec85a6bbc08d320e837abe8a Mon Sep 17 00:00:00 2001 From: Dexoron Date: Wed, 12 Aug 2026 23:35:44 +0800 Subject: [PATCH 17/18] Add Yandex S3 --- .github/workflows/release.yml | 61 ++++++++++++++++++++++++++++++++++- 1 file changed, 60 insertions(+), 1 deletion(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ab1c4a4..a1661d9 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -45,6 +45,10 @@ on: description: "Update Homebrew tap" type: boolean default: false + run_s3: + description: "Mirror Release to Yandex Object Storage (S3)" + type: boolean + default: false permissions: contents: write @@ -1253,4 +1257,59 @@ jobs: # sudo snap install --classic --dangerous dcrup_*.snap # # publish-snap: - # if: ... \ No newline at end of file + # if: ... + + # ── Mirror Release to Yandex Object Storage (S3) ───────────────────────── + publish-s3: + if: | + (github.event_name == 'push' && needs.setup.outputs.prerelease == 'false') || + (github.event_name == 'workflow_dispatch' && github.event.inputs.run_s3 == 'true') + needs: + - setup + - create-release + - man-pages + - build-linux + - build-macos + - build-bsd + - build-windows + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + with: + ref: ${{ needs.setup.outputs.tag }} + + - name: Prepare Directory and Download GitHub Release Assets + id: fetch-assets + shell: bash + run: | + set -euo pipefail + TAG="${{ needs.setup.outputs.tag }}" + + mkdir -p dist + cd dist + + echo "Downloading assets from GitHub Release for tag: ${TAG}" + gh release download "${TAG}" --dir . --repo "${{ github.repository }}" + + cd .. + echo "Archiving source code..." + git archive --format=tar.gz --prefix="dcr-${TAG}/" "${TAG}" -o "dist/dcr-src-${TAG}.tar.gz" + git archive --format=zip --prefix="dcr-${TAG}/" "${TAG}" -o "dist/dcr-src-${TAG}.zip" + + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload Everything to Yandex Object Storage + env: + AWS_ACCESS_KEY_ID: ${{ secrets.AWS_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.AWS_SECRET_ACCESS_KEY }} + AWS_EC2_METADATA_DISABLED: true + shell: bash + run: | + set -euo pipefail + TAG="${{ needs.setup.outputs.tag }}" + + aws s3 sync dist/ "s3://${{ secrets.S3_BUCKET_NAME }}/${TAG}/" \ + --endpoint-url https://storage.yandexcloud.net \ + --region ru-central1 \ No newline at end of file From 54653dfd801531bbe95a26ac49bc684166dc653d Mon Sep 17 00:00:00 2001 From: Dexoron Date: Thu, 13 Aug 2026 12:58:28 +0800 Subject: [PATCH 18/18] feat: add download mirrors and drop legacy install scripts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce `--mirror` and `DCRUP_MIRROR` so `dcrup` can fetch prebuilt binaries from multiple CDNs. Default `auto` tries cf → gh → sf → ya and uses the first successful download. - dcrup.sh / dcrup.ps1: mirror chain, CLI flag, env override - remove install.sh / install.ps1 / install_bsd.sh (superseded by dcrup) - GitHub Actions: upload releases to Cloudflare R2 next to Yandex S3 - docs: README, FAQ, changelog, installation, env vars, contributing --- .github/workflows/release.yml | 16 +- CHANGELOG.md | 24 +++ CONTRIBUTING.md | 1 + FAQ.md | 11 + README.md | 27 ++- dcrup.ps1 | 93 +++++++-- dcrup.sh | 95 +++++++-- docs/README.mdx | 3 +- docs/changelog.mdx | 6 + docs/contributing.mdx | 1 + docs/faq.mdx | 9 + docs/getting-started/installation.mdx | 20 +- docs/reference/environment-variables.mdx | 24 +++ install.ps1 | 160 -------------- install.sh | 253 ----------------------- install_bsd.sh | 241 --------------------- 16 files changed, 295 insertions(+), 689 deletions(-) delete mode 100644 install.ps1 delete mode 100755 install.sh delete mode 100755 install_bsd.sh diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a1661d9..cacb427 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1312,4 +1312,18 @@ jobs: aws s3 sync dist/ "s3://${{ secrets.S3_BUCKET_NAME }}/${TAG}/" \ --endpoint-url https://storage.yandexcloud.net \ - --region ru-central1 \ No newline at end of file + --region ru-central1 + + - name: Upload Everything to Cloudflare R2 + env: + AWS_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }} + AWS_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }} + AWS_EC2_METADATA_DISABLED: true + shell: bash + run: | + set -euo pipefail + TAG="${{ needs.setup.outputs.tag }}" + + aws s3 sync dist/ "s3://${{ secrets.R2_BUCKET_NAME }}/${TAG}/" \ + --endpoint-url https://${{ secrets.R2_ACCOUNT_ID }}.r2.cloudflarestorage.com \ + --region auto \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index 09c18b7..703e324 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,29 @@ # Changelog +## [Unreleased] + +### RU + +**Добавлено:** + +- **Зеркала загрузок в `dcrup`** — `--mirror auto|cf|gh|sf|ya` и `DCRUP_MIRROR`. + По умолчанию `auto`: **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN). + +**Удалено:** + +- **`install.sh` / `install.ps1` / `install_bsd.sh`** — устарели, заменены `dcrup`. + +### EN + +**Added:** + +- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`. + Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN). + +**Removed:** + +- **`install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`. + ## [v0.8.5] - 2026-08-08 "Фиксация версии dcr-version и предупреждения совместимости / Config Version Pinning & Compatibility Warnings" ### RU diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 44b179a..1934ac8 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -11,6 +11,7 @@ Thanks for your interest in the project. ```bash git clone https://github.com/dexoron/dcr.git +# or a source mirror: GitVerse / SourceCraft / SourceForge / GitLab (see README) cd dcr cargo check ``` diff --git a/FAQ.md b/FAQ.md index 66e3d26..3a48470 100644 --- a/FAQ.md +++ b/FAQ.md @@ -61,8 +61,19 @@ Available installation methods may vary by project version. Recommended path: - check instructions in `README.md`; +- use `dcrup install stable` (default mirror chain: **cf → gh → sf → ya**); +- pin a host with `--mirror cf|gh|sf|ya` or `DCRUP_MIRROR` if needed; - if there is no prebuilt package for your platform, build from source using `cargo build --release`. +## How do I choose a download mirror? + +`dcrup` defaults to `--mirror auto`: CloudFlare CDN, then GitHub, then SourceForge, then Yandex CDN. + +```sh +dcrup install stable --mirror cf +export DCRUP_MIRROR=ya +``` + ## How can I get started quickly? Minimal workflow: diff --git a/README.md b/README.md index ca86bf0..806a8e7 100644 --- a/README.md +++ b/README.md @@ -83,7 +83,9 @@ cargo install dcr ```sh curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install export PATH="$HOME/.dcr/bin:$PATH" -dcrup install stable +dcrup install stable # auto: cf → gh → sf → ya +# dcrup install stable --mirror cf # CloudFlare CDN +# dcrup install stable --mirror ya # Yandex CDN (RU) ``` @@ -98,7 +100,9 @@ irm https://ps1.dcr-tool.ru | iex # irm https://ps1.dcr-tool.ru -OutFile dcrup.ps1 # powershell -File .\dcrup.ps1 self-install # $env:Path += ";$env:USERPROFILE\.dcr\bin" -dcrup install stable +dcrup install stable # auto: cf → gh → sf → ya +# dcrup install stable --mirror cf # CloudFlare CDN +# dcrup install stable --mirror ya # Yandex CDN (RU) ``` @@ -218,14 +222,25 @@ Full docs at **[dcr.dexoron.su](https://dcr.dexoron.su)** or in the [`docs/`](do ## Repository Mirrors -The primary repository is hosted on GitHub. The following mirrors are available for source code and releases: +The primary repository is hosted on GitHub. Source and release mirrors: | Region | Mirror | Contents | |---|---|---| -| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) **(recommended)** | Source code | +| Worldwide | [GitHub](https://github.com/dexoron/dcr) | Source code and releases | +| Worldwide | [SourceForge](https://sourceforge.net/projects/dcr-tool/) | Source code and releases | +| Russia | [GitVerse](https://gitverse.ru/dcr_labs/dcr) | Source code | | Russia | [SourceCraft](https://sourcecraft.dev/dexoron-dev/dcr) | Source code | -| Worldwide | [SourceForge](https://sourceforge.net/projects/dcr-tool/) **(recommended)** | Source code and releases | -| Worldwide | [GitLab](https://gitlab.com/dexoron-dev/dcr-tool/dcr) | Source code and releases | + +Release CDN (used by `dcrup`): + +| Code | Host | Role | +|---|---|---| +| `cf` | [cdn-global.dcr-tool.ru](https://cdn-global.dcr-tool.ru) (CloudFlare) | Worldwide CDN | +| `gh` | GitHub Releases | Primary host | +| `sf` | SourceForge | Worldwide fallback | +| `ya` | [cdn-ru.dcr-tool.ru](https://cdn-ru.dcr-tool.ru) (Yandex) | Paid RU fallback for restrictions | + +`dcrup install` defaults to `--mirror auto` and tries **cf → gh → sf → ya**. Override with `--mirror cf|gh|sf|ya` or `DCRUP_MIRROR`. --- diff --git a/dcrup.ps1 b/dcrup.ps1 index a02786e..094b9a9 100644 --- a/dcrup.ps1 +++ b/dcrup.ps1 @@ -6,6 +6,7 @@ $DcrupBin = if ($env:DCRUP_BIN) { $env:DCRUP_BIN } else { Join-Path $DcrupHom $DcrupTc = Join-Path $DcrupHome 'toolchains' $DcrupMeta = Join-Path $DcrupHome 'meta' $RepoUrl = if ($env:DCRUP_REPO) { $env:DCRUP_REPO } else { 'https://github.com/dexoron/dcr' } +$DcrupMirror = if ($env:DCRUP_MIRROR) { $env:DCRUP_MIRROR } else { 'auto' } $ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest' $ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases' $Features = if ($env:DCR_FEATURES) { $env:DCR_FEATURES } else { 'archive' } @@ -20,7 +21,7 @@ function Show-Usage { dcrup — install and switch DCR versions (Windows) Usage: - dcrup install [--build|--release] [--force] + dcrup install [--build|--release] [--force] [--mirror auto|cf|gh|sf|ya] dcrup default dcrup update dcrup list @@ -38,15 +39,24 @@ Spec: night → always build from branch "dev" HEAD Modes: - --release download GitHub Release binary (default for stable/dev) + --release download prebuilt binary (default for stable/dev) --build cargo build --release --features archive night always --build (prebuilt not available) +Mirrors (prebuilt download only): + --mirror auto try cf → gh → sf → ya, first successful download wins (default) + --mirror cf download from cdn-global.dcr-tool.ru + --mirror gh download from github.com/dexoron/dcr + --mirror sf download from sourceforge.net/projects/dcr-tool + --mirror ya download from cdn-ru.dcr-tool.ru + DCRUP_MIRROR env override (default: auto) + Env: DCRUP_HOME default ~/.dcr DCRUP_BIN default $DCRUP_HOME/bin DCR_FEATURES default archive DCRUP_REPO git/GitHub repo URL + DCRUP_MIRROR auto|cf|gh|sf|ya (default: auto) '@ | Write-Host } @@ -123,6 +133,53 @@ function Link-Default([string]$id) { Ok "default → $id ($dst)" } +function Set-Mirror([string]$value) { + if ($value -notin @('auto', 'cf', 'gh', 'sf', 'ya')) { + Die "invalid --mirror: $value (use auto|cf|gh|sf|ya)" + } + $script:DcrupMirror = $value +} + +function Get-MirrorUrl([string]$mirror, [string]$tag, [string]$asset) { + switch ($mirror) { + 'cf' { return "https://cdn-global.dcr-tool.ru/$tag/$asset" } + 'gh' { return "https://github.com/dexoron/dcr/releases/download/$tag/$asset" } + 'sf' { return "https://sourceforge.net/projects/dcr-tool/files/$tag/$asset/download" } + 'ya' { return "https://cdn-ru.dcr-tool.ru/$tag/$asset" } + default { Die "invalid mirror: $mirror" } + } +} + +function Get-MirrorsToTry { + switch ($script:DcrupMirror) { + 'auto' { return @('cf', 'gh', 'sf', 'ya') } + { $_ -in @('cf', 'gh', 'sf', 'ya') } { return @($script:DcrupMirror) } + default { Die "invalid mirror: $($script:DcrupMirror) (use auto|cf|gh|sf|ya)" } + } +} + +function Download-FromMirrors([string]$tag, [string]$asset, [string]$destFile) { + $tmp = "$destFile.download" + foreach ($m in (Get-MirrorsToTry)) { + $url = Get-MirrorUrl $m $tag $asset + Info "trying mirror $m …" + Remove-Item $tmp -ErrorAction SilentlyContinue + try { + Invoke-WebRequest -Uri $url -OutFile $tmp -UserAgent 'dcrup' + $size = (Get-Item $tmp).Length + if ($size -gt 100000) { + Move-Item $tmp $destFile -Force + return $m + } + Warn "mirror $m returned a too-small file ($size bytes)" + } catch { + Warn "mirror $m failed" + } + Remove-Item $tmp -ErrorAction SilentlyContinue + } + return $null +} + function Fetch-Release($channel, $version) { if ($version) { return Invoke-RestMethod -Uri "https://api.github.com/repos/dexoron/dcr/releases/tags/v$version" @@ -142,8 +199,6 @@ function Install-Prebuilt([switch]$Force) { $tag = $rel.tag_name $ver = Normalize-Version $tag $assetName = "dcr-$triple-$ver.exe" - $asset = $rel.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 - if (-not $asset) { Die "asset not found: $assetName" } $id = if ($script:SpecFloating) { Toolchain-Id $ver } else { Toolchain-Id } $dest = Join-Path $DcrupTc $id @@ -155,16 +210,19 @@ function Install-Prebuilt([switch]$Force) { } Ensure-Dirs New-Item -ItemType Directory -Force -Path $dest | Out-Null - Info "downloading $assetName …" - Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $bin + Info "downloading $assetName (mirror=$($script:DcrupMirror)) …" + $mirror = Download-FromMirrors $tag $assetName $bin + if (-not $mirror) { Die "no mirror reachable for $assetName (try: --mirror cf|gh|sf|ya)" } + Info "mirror → $mirror" @" channel=$($script:SpecChannel) version=$ver tag=$tag source=release triple=$triple +mirror=$mirror "@ | Set-Content (Join-Path $dest 'dcrup-meta') - Ok "installed prebuilt $id" + Ok "installed prebuilt $id ($triple via $mirror)" Link-Default $id } @@ -270,23 +328,32 @@ function Cmd-Install { $mode = 'release' $force = $false $spec = $null - foreach ($a in $Args) { + $i = 0 + while ($i -lt $Args.Count) { + $a = $Args[$i] switch -Regex ($a) { - '^--build$' { $mode = 'build'; continue } - '^--release$' { $mode = 'release'; continue } - '^--force$' { $force = $true; continue } + '^--build$' { $mode = 'build' } + '^--release$' { $mode = 'release' } + '^--force$' { $force = $true } + '^--mirror$' { + if ($i + 1 -ge $Args.Count) { Die '--mirror needs auto|cf|gh|sf|ya' } + $i++ + Set-Mirror $Args[$i] + } + '^--mirror=(.+)$' { Set-Mirror $Matches[1] } '^-' { Die "unknown flag: $a" } default { if ($spec) { Die "unexpected: $a" } $spec = $a } } + $i++ } - if (-not $spec) { Die 'usage: dcrup install [--build|--release] [--force]' } + if (-not $spec) { Die 'usage: dcrup install [--build|--release] [--force] [--mirror auto|cf|gh|sf|ya]' } Parse-Spec $spec Ensure-Dirs if ($script:SpecChannel -eq 'night') { $mode = 'build' } - Info "install spec=$spec channel=$($script:SpecChannel) mode=$mode" + Info "install spec=$spec channel=$($script:SpecChannel) mode=$mode mirror=$($script:DcrupMirror)" if ($mode -eq 'build') { Install-Build -Force:$force } else { Install-Prebuilt -Force:$force } Ensure-UserPath Install-CmdShim diff --git a/dcrup.sh b/dcrup.sh index 33cc7e4..7f0bc7a 100755 --- a/dcrup.sh +++ b/dcrup.sh @@ -6,6 +6,7 @@ DCRUP_BIN="${DCRUP_BIN:-$DCRUP_HOME/bin}" DCRUP_TC="${DCRUP_HOME}/toolchains" DCRUP_META="${DCRUP_HOME}/meta" REPO_URL="${DCRUP_REPO:-https://github.com/dexoron/dcr}" +DCRUP_MIRROR="${DCRUP_MIRROR:-auto}" API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest" API_ALL="https://api.github.com/repos/dexoron/dcr/releases" FEATURES="${DCR_FEATURES:-archive}" @@ -26,7 +27,7 @@ usage() { dcrup — install and switch DCR versions Usage: - dcrup install [--build|--release] [--force] [--libc gnu|musl|auto] + dcrup install [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya] dcrup default dcrup update dcrup list @@ -44,7 +45,7 @@ Spec: night → always build from branch "dev" HEAD Modes: - --release download GitHub Release binary (default for stable/dev) + --release download prebuilt binary (default for stable/dev) --build cargo build --release --features archive night always --build (prebuilt not available) @@ -53,12 +54,21 @@ Linux libc (prebuilt triple only): --libc musl x86_64-unknown-linux-musl --libc auto musl if host ldd/libc is musl, else gnu +Mirrors (prebuilt download only): + --mirror auto try cf → gh → sf → ya, first successful download wins (default) + --mirror cf download from cdn-global.dcr-tool.ru + --mirror gh download from github.com/dexoron/dcr + --mirror sf download from sourceforge.net/projects/dcr-tool + --mirror ya download from cdn-ru.dcr-tool.ru + DCRUP_MIRROR env override (default: auto) + Env: DCRUP_HOME default ~/.dcr DCRUP_BIN default $DCRUP_HOME/bin DCR_FEATURES default archive DCRUP_REPO git/GitHub repo URL DCRUP_LIBC gnu|musl|auto (default: gnu) + DCRUP_MIRROR auto|cf|gh|sf|ya (default: auto) EOF } @@ -239,9 +249,57 @@ json_tag() { printf '%s' "$1" | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n1 } -json_asset_url() { - local json="$1" asset="$2" - printf '%s' "$json" | sed -n "s#.*\"browser_download_url\"[[:space:]]*:[[:space:]]*\"\([^\"]*/${asset}\)\"#\1#p" | head -n1 +set_mirror() { + case "$1" in + auto|cf|gh|sf|ya) DCRUP_MIRROR="$1" ;; + *) die "invalid --mirror: $1 (use auto|cf|gh|sf|ya)" ;; + esac +} + +mirror_url() { + local mirror="$1" tag="$2" asset="$3" + case "$mirror" in + cf) printf 'https://cdn-global.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; + gh) printf 'https://github.com/dexoron/dcr/releases/download/%s/%s' "$tag" "$asset" ;; + sf) printf 'https://sourceforge.net/projects/dcr-tool/files/%s/%s/download' "$tag" "$asset" ;; + ya) printf 'https://cdn-ru.dcr-tool.ru/%s/%s' "$tag" "$asset" ;; + *) return 1 ;; + esac +} + +mirror_chain() { echo "cf gh sf ya"; } + +mirrors_to_try() { + case "$DCRUP_MIRROR" in + auto) mirror_chain ;; + cf|gh|sf|ya) echo "$DCRUP_MIRROR" ;; + *) die "invalid mirror: $DCRUP_MIRROR (use auto|cf|gh|sf|ya)" ;; + esac +} + +download_from_mirrors() { + local tag="$1" asset="$2" dest="$3" + local tmp="$dest/dcr.download" m url size + USED_MIRROR="" + for m in $(mirrors_to_try); do + url="$(mirror_url "$m" "$tag" "$asset")" + log "trying mirror $m …" + rm -f "$tmp" + if curl -fL --retry 2 --retry-delay 1 --connect-timeout 10 --max-time 180 \ + -A "dcrup" "$url" -o "$tmp"; then + size="$(wc -c <"$tmp" | tr -d ' ')" + if [[ "${size:-0}" -gt 100000 ]]; then + mv "$tmp" "$dest/dcr" + USED_MIRROR="$m" + return 0 + fi + warn "mirror $m returned a too-small file (${size:-0} bytes)" + else + warn "mirror $m failed" + fi + rm -f "$tmp" + done + return 1 } latest_stable_json() { @@ -281,7 +339,7 @@ release_json_for_spec() { } install_prebuilt() { - local triple asset tag version url json id dest + local triple asset tag version id dest mirror need_cmd curl triple="$(detect_triple)" json="$(release_json_for_spec)" @@ -292,8 +350,6 @@ install_prebuilt() { case "$triple" in *windows*) asset="${asset}.exe" ;; esac - url="$(json_asset_url "$json" "$asset")" - [[ -n "$url" ]] || die "asset not found: $asset (tag $tag)" id="$(toolchain_id "$version")" dest="$DCRUP_TC/$id" @@ -303,9 +359,12 @@ install_prebuilt() { return 0 fi - log "downloading $asset …" mkdir -p "$dest" - curl -fL "$url" -o "$dest/dcr" + log "downloading $asset (mirror=$DCRUP_MIRROR) …" + download_from_mirrors "$tag" "$asset" "$dest" \ + || die "no mirror reachable for $asset (try: --mirror cf|gh|sf|ya)" + mirror="$USED_MIRROR" + log "mirror → $mirror" chmod +x "$dest/dcr" printf '%s\n' \ "channel=$SPEC_CHANNEL" \ @@ -313,9 +372,10 @@ install_prebuilt() { "tag=$tag" \ "source=release" \ "triple=$triple" \ + "mirror=$mirror" \ "libc=${DCRUP_LIBC:-gnu}" \ >"$dest/dcrup-meta" - ok "installed prebuilt $id ($triple)" + ok "installed prebuilt $id ($triple via $mirror)" link_default "$id" } @@ -405,6 +465,15 @@ cmd_install() { libc_cli="${1#--libc=}" shift ;; + --mirror) + [[ $# -ge 2 ]] || die "--mirror needs auto|cf|gh|sf|ya" + set_mirror "$2" + shift 2 + ;; + --mirror=*) + set_mirror "${1#--mirror=}" + shift + ;; -h|--help) usage; exit 0 ;; -*) die "unknown flag: $1" ;; *) @@ -414,7 +483,7 @@ cmd_install() { ;; esac done - [[ -n "$spec" ]] || die "usage: dcrup install [--build|--release] [--force] [--libc gnu|musl|auto]" + [[ -n "$spec" ]] || die "usage: dcrup install [--build|--release] [--force] [--libc gnu|musl|auto] [--mirror auto|cf|gh|sf|ya]" parse_spec "$spec" ensure_dirs @@ -431,7 +500,7 @@ cmd_install() { fi export FORCE - log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC" + log "install spec=$SPEC_RAW channel=$SPEC_CHANNEL version=${SPEC_VERSION:-∅} mode=$mode libc=$DCRUP_LIBC mirror=$DCRUP_MIRROR" if [[ "$mode" == "build" ]]; then install_build else diff --git a/docs/README.mdx b/docs/README.mdx index f4e4d7f..fb048f9 100644 --- a/docs/README.mdx +++ b/docs/README.mdx @@ -11,7 +11,8 @@ sidebar_label: Introduction # Installation (dcrup) curl -fsSL https://sh.dcr-tool.ru | sh -s -- self-install export PATH="$HOME/.dcr/bin:$PATH" -dcrup install stable +dcrup install stable # auto: cf → gh → sf → ya +# dcrup install stable --mirror cf # CloudFlare / GitHub / SourceForge / Yandex # Create a project dcr new my-app diff --git a/docs/changelog.mdx b/docs/changelog.mdx index f80dfd9..f306e58 100644 --- a/docs/changelog.mdx +++ b/docs/changelog.mdx @@ -4,6 +4,12 @@ sidebar_label: Changelog # Changelog +## Unreleased + +- **Download mirrors in `dcrup`** — `--mirror auto|cf|gh|sf|ya` and `DCRUP_MIRROR`. + Default `auto` tries **cf → gh → sf → ya** (CloudFlare CDN, GitHub, SourceForge, Yandex CDN). +- **Removed `install.sh` / `install.ps1` / `install_bsd.sh`** — superseded by `dcrup`. + ## 0.8.5 (2026-08-08) - **`package.dcr-version`** — optional CLI version pin in `dcr.toml`. diff --git a/docs/contributing.mdx b/docs/contributing.mdx index 8883c5b..8a71166 100644 --- a/docs/contributing.mdx +++ b/docs/contributing.mdx @@ -8,6 +8,7 @@ sidebar_label: Contributing ```bash git clone https://github.com/dexoron/dcr +# or a source mirror: GitVerse / SourceCraft / SourceForge / GitLab (see README) cd dcr cargo build ``` diff --git a/docs/faq.mdx b/docs/faq.mdx index 4680f99..20673a4 100644 --- a/docs/faq.mdx +++ b/docs/faq.mdx @@ -61,6 +61,15 @@ Use `kind = "flat-bin"` (NASM `-f bin`) and optional `[archive]` for a FAT disk See [OS-dev recipe](/docs/recipes/os-flat-bin-archive). +## How do I choose a download mirror? + +`dcrup install` uses `--mirror auto` by default and tries **cf → gh → sf → ya**. + +```bash +dcrup install stable --mirror cf +export DCRUP_MIRROR=ya +``` + ## Lock file `dcr.lock` is created during builds (`dcr build`) when registry dependencies are present. To force an update — delete `dcr.lock` and run `dcr build`. diff --git a/docs/getting-started/installation.mdx b/docs/getting-started/installation.mdx index edfb38d..9b9a26f 100644 --- a/docs/getting-started/installation.mdx +++ b/docs/getting-started/installation.mdx @@ -45,10 +45,12 @@ After `self-install`, the command is **`dcrup`** (no `.sh` / `.ps1`): shims live ### Common dcrup commands ```sh -dcrup install stable # latest stable prebuilt +dcrup install stable # latest stable prebuilt (auto: cf → gh → sf → ya) dcrup install 0.8.2 # pin → 0.8.2@stable dcrup install 0.8.2@dev dcrup install stable --libc musl # Linux: musl asset (default: gnu) +dcrup install stable --mirror cf # CloudFlare CDN +dcrup install stable --mirror ya # Yandex CDN (RU fallback) dcrup install stable --build # cargo build --features archive dcrup install night # always build from branch dev HEAD dcrup default 0.8.2 @@ -60,6 +62,22 @@ dcrup which Layout: `~/.dcr/toolchains//dcr` and `~/.dcr/bin/dcr` → active version. +### Download mirrors + +Prebuilt installs try **cf → gh → sf → ya** unless you pin a host. + +| Code | URL | Notes | +|------|-----|--------| +| `cf` | `https://cdn-global.dcr-tool.ru//` | CloudFlare CDN (worldwide) | +| `gh` | `https://github.com/dexoron/dcr/releases/download//` | GitHub Releases | +| `sf` | `https://sourceforge.net/projects/dcr-tool/files///download` | SourceForge | +| `ya` | `https://cdn-ru.dcr-tool.ru//` | Yandex CDN (RU fallback) | + +```sh +dcrup install stable --mirror auto # default +export DCRUP_MIRROR=cf # persist for this shell +``` + --- ## Arch Linux (AUR) diff --git a/docs/reference/environment-variables.mdx b/docs/reference/environment-variables.mdx index 1c70f9e..406fdbc 100644 --- a/docs/reference/environment-variables.mdx +++ b/docs/reference/environment-variables.mdx @@ -57,3 +57,27 @@ export DCR_INDEX_PATH=/custom/path/index.json ``` Default: `~/.dcr/index.json`. + +## DCRUP_MIRROR + +Download mirror for `dcrup` (prebuilt binaries only). + +```bash +export DCRUP_MIRROR=auto # cf → gh → sf → ya (default) +export DCRUP_MIRROR=cf # CloudFlare CDN +export DCRUP_MIRROR=gh # GitHub Releases +export DCRUP_MIRROR=sf # SourceForge +export DCRUP_MIRROR=ya # Yandex CDN (RU fallback) +``` + +CLI override: `dcrup install stable --mirror cf`. + +## DCRUP_HOME / DCRUP_BIN / DCRUP_REPO + +Installer layout and source clone URL: + +```bash +export DCRUP_HOME=~/.dcr +export DCRUP_BIN=~/.dcr/bin +export DCRUP_REPO=https://github.com/dexoron/dcr +``` diff --git a/install.ps1 b/install.ps1 deleted file mode 100644 index 11eaca9..0000000 --- a/install.ps1 +++ /dev/null @@ -1,160 +0,0 @@ -$ErrorActionPreference = 'Stop' - -$RepoUrl = 'https://github.com/dexoron/dcr' -$ApiLatest = 'https://api.github.com/repos/dexoron/dcr/releases/latest' -$ApiAll = 'https://api.github.com/repos/dexoron/dcr/releases' -$InstallPath = Join-Path $env:LOCALAPPDATA 'dcr' -$BinPath = Join-Path $env:USERPROFILE '.local\bin' -$BinaryPath = Join-Path $InstallPath 'dcr.exe' - -function Info($msg) { Write-Host "[INFO] $msg" -ForegroundColor Cyan } -function Ok($msg) { Write-Host "[OK] $msg" -ForegroundColor Green } -function Warn($msg) { Write-Host "[WARN] $msg" -ForegroundColor Yellow } -function Fail($msg) { Write-Host "[ERROR] $msg" -ForegroundColor Red; exit 1 } - -function Require-Cmd($name) { - if (-not (Get-Command $name -ErrorAction SilentlyContinue)) { - Fail "Not found: $name" - } -} - -function Select-Channel { - Write-Host "Choose channel:" - Write-Host " 1) Latest stable release (default)" - Write-Host " 2) Latest dev (pre-release)" - $choice = Read-Host "Enter 1 or 2 [1]" - if ([string]::IsNullOrWhiteSpace($choice)) { $choice = '1' } - - switch ($choice) { - '1' { return 'stable' } - '2' { return 'dev' } - default { Fail 'Unknown option' } - } -} - -function Select-Mode { - Write-Host "Choose installation mode:" - Write-Host " 1) Download prebuilt binary from GitHub Release (recommended)" - Write-Host " 2) Build from git" - $choice = Read-Host "Enter 1 or 2 [1]" - if ([string]::IsNullOrWhiteSpace($choice)) { $choice = '1' } - - switch ($choice) { - '1' { return 'release' } - '2' { return 'build' } - default { Fail 'Unknown option' } - } -} - -function Get-Target { - $arch = $env:PROCESSOR_ARCHITECTURE - if ($env:PROCESSOR_ARCHITEW6432) { - $arch = $env:PROCESSOR_ARCHITEW6432 - } - switch ($arch) { - 'AMD64' { return 'x86_64-pc-windows-msvc' } - 'ARM64' { return 'aarch64-pc-windows-msvc' } - 'x86' { return 'i686-pc-windows-msvc' } - default { Fail "Unsupported Windows architecture: $arch" } - } -} - -function Fetch-ReleaseJson($channel) { - if ($channel -eq 'dev') { - Info "Looking for latest dev (pre-release)..." - $releases = Invoke-RestMethod -Method Get -Uri $ApiAll - $pre = $releases | Where-Object { $_.prerelease -eq $true } | Select-Object -First 1 - if (-not $pre) { Fail "No dev (pre-release) found on GitHub" } - return $pre - } else { - return Invoke-RestMethod -Method Get -Uri $ApiLatest - } -} - -function Install-FromRelease { - param([string]$target, [string]$channel) - - New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null - - $release = Fetch-ReleaseJson $channel - $tag = $release.tag_name - $version = $tag.TrimStart('v') - - # Имя бинарника: dcr--.exe - $assetName = "dcr-$target-$version.exe" - - Info "Fetching release $tag (channel: $channel)..." - - $asset = $release.assets | Where-Object { $_.name -eq $assetName } | Select-Object -First 1 - if (-not $asset) { - Fail "Asset $assetName not found in release $tag" - } - - Info "Downloading $assetName..." - Invoke-WebRequest -Uri $asset.browser_download_url -OutFile $BinaryPath - Ok "Binary downloaded: $assetName" -} - -function Install-FromSource { - param([string]$target, [string]$channel) - - Require-Cmd 'git' - Require-Cmd 'cargo' - - $tmp = Join-Path $env:TEMP 'dcr-install' - if (Test-Path $tmp) { Remove-Item -Recurse -Force $tmp } - - Info 'Cloning repository...' - if ($channel -eq 'dev') { - # Пробуем ветку dev - $result = git clone --depth 1 --branch dev $RepoUrl $tmp 2>&1 - if ($LASTEXITCODE -ne 0) { - git clone --depth 1 $RepoUrl $tmp | Out-Null - } - } else { - git clone --depth 1 $RepoUrl $tmp | Out-Null - } - - Info 'Building release binary...' - Push-Location $tmp - cargo build --release --target $target - Pop-Location - - New-Item -ItemType Directory -Path $InstallPath -Force | Out-Null - Copy-Item (Join-Path $tmp "target\$target\release\dcr.exe") $BinaryPath -Force - Ok 'Binary installed from source' - - Remove-Item -Recurse -Force $tmp -} - -function Setup-Path { - New-Item -ItemType Directory -Path $BinPath -Force | Out-Null - $linkPath = Join-Path $BinPath 'dcr.exe' - Copy-Item $BinaryPath $linkPath -Force - - $userPath = [Environment]::GetEnvironmentVariable('Path', 'User') - if (-not $userPath) { $userPath = '' } - - if (-not ($userPath -split ';' | Where-Object { $_ -eq $BinPath })) { - $newPath = if ($userPath) { "$userPath;$BinPath" } else { $BinPath } - [Environment]::SetEnvironmentVariable('Path', $newPath, 'User') - $env:Path = "$env:Path;$BinPath" - Warn "Added path to PATH: $BinPath" - } - - Ok "dcr command is available via $linkPath" -} - -Info 'Starting DCR installation' -$channel = Select-Channel -$mode = Select-Mode -$target = Get-Target - -if ($mode -eq 'build') { - Install-FromSource -target $target -channel $channel -} else { - Install-FromRelease -target $target -channel $channel -} - -Setup-Path -Ok 'Installation completed successfully' \ No newline at end of file diff --git a/install.sh b/install.sh deleted file mode 100755 index b600c3b..0000000 --- a/install.sh +++ /dev/null @@ -1,253 +0,0 @@ -#!/usr/bin/env bash -set -Eeuo pipefail - -TMPDIR="/tmp/dcr-install" -INSTALL_PATH="$HOME/.local/share/dcr" -BINPATH="$HOME/.local/bin" -MANPATH="${XDG_DATA_HOME:-$HOME/.local/share}/man" -LOGFILE="$HOME/.cache/dcr-install.log" -REPO_URL="https://github.com/dexoron/dcr" -GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest" -GITHUB_API_ALL="https://api.github.com/repos/dexoron/dcr/releases" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -mkdir -p "$(dirname "$LOGFILE")" -exec > >(tee -a "$LOGFILE") 2>&1 - -log() { echo -e "${BLUE}[$(date '+%Y-%m-%d %H:%M:%S')]${NC} $1"; } -success() { echo -e "${GREEN}✔ $1${NC}"; } -warn() { echo -e "${YELLOW}⚠ $1${NC}"; } -error() { echo -e "${RED}✖ $1${NC}"; } - -trap 'error "Error on line $LINENO"; exit 1' ERR - -INSTALL_MODE="" -CHANNEL="" - -check_os() { - case "$(uname -s)" in - Linux|Darwin|FreeBSD|OpenBSD|NetBSD) ;; - *) error "Only Linux, macOS, and BSD systems are supported"; exit 1 ;; - esac -} - -detect_target() { - local os arch - os="$(uname -s)" - arch="$(uname -m)" - - case "$os:$arch" in - Linux:x86_64) TARGET_TRIPLE="x86_64-unknown-linux-gnu" ;; - Linux:aarch64|Linux:arm64) TARGET_TRIPLE="aarch64-unknown-linux-gnu" ;; - Linux:i686|Linux:i386) TARGET_TRIPLE="i686-unknown-linux-gnu" ;; - Linux:armv7l|Linux:armv7) TARGET_TRIPLE="armv7-unknown-linux-gnueabihf" ;; - Linux:riscv64) TARGET_TRIPLE="riscv64gc-unknown-linux-gnu" ;; - Darwin:x86_64) TARGET_TRIPLE="x86_64-apple-darwin" ;; - Darwin:arm64|Darwin:aarch64) TARGET_TRIPLE="aarch64-apple-darwin" ;; - FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;; - FreeBSD:aarch64|FreeBSD:arm64) TARGET_TRIPLE="aarch64-unknown-freebsd" ;; - FreeBSD:i686|FreeBSD:i386) TARGET_TRIPLE="i686-unknown-freebsd" ;; - OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;; - OpenBSD:aarch64|OpenBSD:arm64) TARGET_TRIPLE="aarch64-unknown-openbsd" ;; - OpenBSD:i686|OpenBSD:i386) TARGET_TRIPLE="i686-unknown-openbsd" ;; - NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;; - NetBSD:aarch64|NetBSD:arm64) TARGET_TRIPLE="aarch64-unknown-netbsd" ;; - NetBSD:i686|NetBSD:i386) TARGET_TRIPLE="i686-unknown-netbsd" ;; - - *) error "Unsupported platform: $os/$arch"; exit 1 ;; - esac -} - -check_common_dependencies() { - command -v curl >/dev/null 2>&1 || { error "curl is not installed"; exit 1; } - if [[ "$CHANNEL" == "dev" ]]; then - if ! command -v python3 >/dev/null 2>&1 && ! command -v jq >/dev/null 2>&1; then - error "For dev installations, either 'python3' or 'jq' must be installed to parse GitHub API response." - exit 1 - fi - fi -} - -check_build_dependencies() { - command -v git >/dev/null 2>&1 || { error "git is not installed"; exit 1; } - command -v cargo >/dev/null 2>&1 || { error "cargo is not installed"; exit 1; } -} - -select_channel() { - echo "Choose channel:" - echo " 1) Latest stable release (default)" - echo " 2) Latest dev (pre-release)" - read -r -p "Enter 1 or 2 [1]: " choice < /dev/tty - - case "${choice:-1}" in - 1) CHANNEL="stable" ;; - 2) CHANNEL="dev" ;; - *) error "Unknown option"; exit 1 ;; - esac -} - -select_install_mode() { - echo "Choose installation mode:" - echo " 1) Download prebuilt binary from GitHub Release (recommended)" - echo " 2) Build from git" - read -r -p "Enter 1 or 2 [1]: " choice < /dev/tty - - case "${choice:-1}" in - 1) INSTALL_MODE="release" ;; - 2) INSTALL_MODE="build" ;; - *) error "Unknown option"; exit 1 ;; - esac -} - -# Returns JSON of the requested release to stdout -fetch_release_json() { - if [[ "$CHANNEL" == "dev" ]]; then - log "Looking for latest dev (pre-release)..." - local json result - json="$(curl -fsSL "$GITHUB_API_ALL")" - if command -v jq >/dev/null 2>&1; then - result="$(printf '%s' "$json" | jq -e -c 'map(select(.prerelease == true))[0]')" - else - result="$(printf '%s' "$json" | python3 - <<'EOF' -import sys, json -releases = json.load(sys.stdin) -pre = [r for r in releases if r.get("prerelease", False)] -if not pre: - print("{}", end="") -else: - print(json.dumps(pre[0]), end="") -EOF -)" - fi - - if [[ -z "$result" || "$result" == "{}" || "$result" == "null" ]]; then - error "No dev (pre-release) found on GitHub" - exit 1 - fi - printf '%s' "$result" - else - curl -fsSL "$GITHUB_API_LATEST" - fi -} - -download_binary() { - local release_json tag version asset_name download_url - - release_json="$(fetch_release_json)" - - tag="$(printf '%s\n' "$release_json" | \ - sed -n 's/.*"tag_name": "\([^"]*\)".*/\1/p' | head -n1)" - if [[ -z "$tag" ]]; then - error "Failed to determine release version" - exit 1 - fi - - # Version without leading 'v' - version="${tag#v}" - # Binary name: dcr-- - asset_name="dcr-${TARGET_TRIPLE}-${version}" - - log "Fetching release ${tag} (channel: ${CHANNEL})..." - - download_url="$(printf '%s\n' "$release_json" | \ - sed -n "s#.*\"browser_download_url\": \"\([^\"]*/${asset_name}\)\".*#\1#p" | head -n1)" - - if [[ -z "$download_url" ]]; then - error "Asset ${asset_name} not found in release ${tag}" - exit 1 - fi - - mkdir -p "$INSTALL_PATH" - curl -fL "$download_url" -o "$INSTALL_PATH/dcr" - chmod +x "$INSTALL_PATH/dcr" - success "Binary ${asset_name} downloaded (${tag})" -} - -prepare_sources() { - log "Fetching sources..." - rm -rf "$TMPDIR" - git clone --depth 1 "$REPO_URL" "$TMPDIR" - if [[ "$CHANNEL" == "dev" ]]; then - # Clone dev branch if it exists - git clone --depth 1 --branch dev "$REPO_URL" "$TMPDIR" 2>/dev/null || \ - git clone --depth 1 "$REPO_URL" "$TMPDIR" - fi - success "Sources fetched" -} - -build_binary() { - log "Building release binary..." - (cd "$TMPDIR" && cargo build --release) - success "Build completed" -} - -install_built_binary() { - mkdir -p "$INSTALL_PATH" - cp "$TMPDIR/target/release/dcr" "$INSTALL_PATH/dcr" - chmod +x "$INSTALL_PATH/dcr" - success "Binary installed from source" -} - -install_link() { - log "Creating symlink..." - mkdir -p "$BINPATH" - ln -sf "$INSTALL_PATH/dcr" "$BINPATH/dcr" - success "Command 'dcr' added to $BINPATH" -} - -install_man() { - log "Installing man pages..." - local man_src="$TMPDIR/man/man1" - if [[ -d "$man_src" ]]; then - mkdir -p "$MANPATH/man1" - cp "$man_src"/*.1 "$MANPATH/man1/" - success "Man pages installed to $MANPATH/man1" - fi -} - -check_path() { - if ! echo "$PATH" | grep -q "$BINPATH"; then - warn "Directory $BINPATH not found in PATH" - echo "Add this to ~/.bashrc or ~/.zshrc:" - echo "export PATH=\"$BINPATH:\$PATH\"" - fi -} - -cleanup() { - rm -rf "$TMPDIR" 2>/dev/null || true -} - -main() { - log "Starting DCR installation" - - check_os - detect_target - select_channel - check_common_dependencies - select_install_mode - cleanup - - if [[ "$INSTALL_MODE" == "build" ]]; then - check_build_dependencies - prepare_sources - build_binary - install_built_binary - else - download_binary - fi - - install_link - install_man - check_path - cleanup - - success "Installation completed successfully" - log "Log saved to $LOGFILE" -} - -main "$@" diff --git a/install_bsd.sh b/install_bsd.sh deleted file mode 100755 index e793c9b..0000000 --- a/install_bsd.sh +++ /dev/null @@ -1,241 +0,0 @@ -#!/bin/sh -set -eu - -TMPDIR="/tmp/dcr-install" -INSTALL_PATH="$HOME/.local/share/dcr" -BINPATH="$HOME/.local/bin" -MANPATH="${XDG_DATA_HOME:-$HOME/.local/share}/man" -LOGFILE="$HOME/.cache/dcr-install.log" -REPO_URL="https://github.com/dexoron/dcr" -GITHUB_API_LATEST="https://api.github.com/repos/dexoron/dcr/releases/latest" -GITHUB_API_ALL="https://api.github.com/repos/dexoron/dcr/releases" - -RED='\033[0;31m' -GREEN='\033[0;32m' -YELLOW='\033[1;33m' -BLUE='\033[0;34m' -NC='\033[0m' - -mkdir -p "$(dirname "$LOGFILE")" - -log() { printf "${BLUE}[%s]${NC} %s\n" "$(date '+%Y-%m-%d %H:%M:%S')" "$1" | tee -a "$LOGFILE"; } -success() { printf "${GREEN}✔ %s${NC}\n" "$1" | tee -a "$LOGFILE"; } -warn() { printf "${YELLOW}⚠ %s${NC}\n" "$1" | tee -a "$LOGFILE"; } -error() { printf "${RED}✖ %s${NC}\n" "$1" | tee -a "$LOGFILE" >&2; } - -INSTALL_MODE="" -CHANNEL="" - -check_os() { - case "$(uname -s)" in - FreeBSD|OpenBSD|NetBSD|DragonFly) ;; - *) error "Only BSD systems (FreeBSD, OpenBSD, NetBSD, DragonFly) are supported by this script"; exit 1 ;; - esac -} - -detect_target() { - local os arch - os="$(uname -s)" - arch="$(uname -m)" - - case "$os:$arch" in - FreeBSD:x86_64|FreeBSD:amd64) TARGET_TRIPLE="x86_64-unknown-freebsd" ;; - FreeBSD:aarch64|FreeBSD:arm64) TARGET_TRIPLE="aarch64-unknown-freebsd" ;; - FreeBSD:i686|FreeBSD:i386) TARGET_TRIPLE="i686-unknown-freebsd" ;; - OpenBSD:x86_64|OpenBSD:amd64) TARGET_TRIPLE="x86_64-unknown-openbsd" ;; - OpenBSD:aarch64|OpenBSD:arm64) TARGET_TRIPLE="aarch64-unknown-openbsd" ;; - OpenBSD:i686|OpenBSD:i386) TARGET_TRIPLE="i686-unknown-openbsd" ;; - NetBSD:x86_64|NetBSD:amd64) TARGET_TRIPLE="x86_64-unknown-netbsd" ;; - NetBSD:aarch64|NetBSD:arm64) TARGET_TRIPLE="aarch64-unknown-netbsd" ;; - NetBSD:i686|NetBSD:i386) TARGET_TRIPLE="i686-unknown-netbsd" ;; - DragonFly:x86_64|DragonFly:amd64) TARGET_TRIPLE="x86_64-unknown-dragonfly" ;; - *) error "Unsupported BSD platform: $os/$arch"; exit 1 ;; - esac -} - -check_common_dependencies() { - command -v curl >/dev/null 2>&1 || { error "curl is not installed"; exit 1; } - if [ "$CHANNEL" = "dev" ]; then - if ! command -v python3 >/dev/null 2>&1 && ! command -v jq >/dev/null 2>&1; then - error "For dev installations, either 'python3' or 'jq' must be installed to parse GitHub API response." - exit 1 - fi - fi -} - -check_build_dependencies() { - command -v git >/dev/null 2>&1 || { error "git is not installed"; exit 1; } - command -v cargo >/dev/null 2>&1 || { error "cargo is not installed"; exit 1; } -} - -select_channel() { - echo "Choose channel:" - echo " 1) Latest stable release (default)" - echo " 2) Latest dev (pre-release)" - printf "Enter 1 or 2 [1]: " - read -r choice - - case "${choice:-1}" in - 1) CHANNEL="stable" ;; - 2) CHANNEL="dev" ;; - *) error "Unknown option"; exit 1 ;; - esac -} - -select_install_mode() { - echo "Choose installation mode:" - echo " 1) Download prebuilt binary from GitHub Release (recommended)" - echo " 2) Build from git" - printf "Enter 1 or 2 [1]: " - read -r choice - - case "${choice:-1}" in - 1) INSTALL_MODE="release" ;; - 2) INSTALL_MODE="build" ;; - *) error "Unknown option"; exit 1 ;; - esac -} - -fetch_release_json() { - if [ "$CHANNEL" = "dev" ]; then - log "Looking for latest dev (pre-release)..." - local json result - json="$(curl -fsSL "$GITHUB_API_ALL")" - if command -v jq >/dev/null 2>&1; then - result="$(printf '%s' "$json" | jq -e -c 'map(select(.prerelease == true))[0]')" - else - result="$(printf '%s' "$json" | python3 - <<'EOF' -import sys, json -releases = json.load(sys.stdin) -pre = [r for r in releases if r.get("prerelease", False)] -if not pre: - print("{}", end="") -else: - print(json.dumps(pre[0]), end="") -EOF -)" - fi - - if [ -z "$result" ] || [ "$result" = "{}" ] || [ "$result" = "null" ]; then - error "No dev (pre-release) found on GitHub" - exit 1 - fi - printf '%s' "$result" - else - curl -fsSL "$GITHUB_API_LATEST" - fi -} - -download_binary() { - local release_json tag version asset_name download_url - - release_json="$(fetch_release_json)" - - tag="$(printf '%s\n' "$release_json" | \ - sed -n 's/.*"tag_name": "\([^"]*\)".*/\1/p' | head -n1)" - if [ -z "$tag" ]; then - error "Failed to determine release version" - exit 1 - fi - - version="${tag#v}" - asset_name="dcr-${TARGET_TRIPLE}-${version}" - - log "Fetching release ${tag} (channel: ${CHANNEL})..." - - download_url="$(printf '%s\n' "$release_json" | \ - sed -n "s#.*\"browser_download_url\": \"\([^\"]*/${asset_name}\)\".*#\1#p" | head -n1)" - - if [ -z "$download_url" ]; then - error "Asset ${asset_name} not found in release ${tag}" - exit 1 - fi - - mkdir -p "$INSTALL_PATH" - curl -fL "$download_url" -o "$INSTALL_PATH/dcr" - chmod +x "$INSTALL_PATH/dcr" - success "Binary ${asset_name} downloaded (${tag})" -} - -prepare_sources() { - log "Fetching sources..." - rm -rf "$TMPDIR" - git clone --depth 1 "$REPO_URL" "$TMPDIR" - if [ "$CHANNEL" = "dev" ]; then - git clone --depth 1 --branch dev "$REPO_URL" "$TMPDIR" 2>/dev/null || \ - git clone --depth 1 "$REPO_URL" "$TMPDIR" - fi - success "Sources fetched" -} - -build_binary() { - log "Building release binary..." - (cd "$TMPDIR" && cargo build --release) - success "Build completed" -} - -install_built_binary() { - mkdir -p "$INSTALL_PATH" - cp "$TMPDIR/target/release/dcr" "$INSTALL_PATH/dcr" - chmod +x "$INSTALL_PATH/dcr" - success "Binary installed from source" -} - -install_link() { - log "Creating symlink..." - mkdir -p "$BINPATH" - ln -sf "$INSTALL_PATH/dcr" "$BINPATH/dcr" - success "Command 'dcr' added to $BINPATH" -} - -install_man() { - log "Installing man pages..." - man_src="$TMPDIR/man/man1" - if [ -d "$man_src" ]; then - mkdir -p "$MANPATH/man1" - cp "$man_src"/*.1 "$MANPATH/man1/" - success "Man pages installed to $MANPATH/man1" - fi -} - -check_path() { - if ! echo "$PATH" | grep -q "$BINPATH"; then - warn "Directory $BINPATH not found in PATH" - echo "Add this to ~/.bashrc, ~/.zshrc or ~/.profile:" - echo "export PATH=\"$BINPATH:\$PATH\"" - fi -} - -cleanup() { - rm -rf "$TMPDIR" 2>/dev/null || true -} - -main() { - log "Starting DCR installation for BSD" - - check_os - select_channel - check_common_dependencies - detect_target - select_install_mode - cleanup - - if [ "$INSTALL_MODE" = "build" ]; then - check_build_dependencies - prepare_sources - build_binary - install_built_binary - else - download_binary - fi - - install_link - install_man - check_path - cleanup - - success "Installation completed successfully" - log "Log saved to $LOGFILE" -} - -main "$@"