diff --git a/.gitea/workflows/morskaRelease.yml b/.gitea/workflows/morskaRelease.yml index 13d2afa..9a2b22c 100644 --- a/.gitea/workflows/morskaRelease.yml +++ b/.gitea/workflows/morskaRelease.yml @@ -1,96 +1,59 @@ -name: ReleaseApp (auto from latest successful BuildApp) +name: ReleaseApp (JS finder + download) on: - workflow_dispatch: {} # Manual trigger with ZERO inputs + workflow_dispatch: {} jobs: release: runs-on: ubuntu-latest env: - # --- Adjust to your instance/repo if needed --- GITEA_BASE_URL: https://code.bim-it.pl OWNER: mz REPO: DiunaBI - WORKFLOW_NAME: BuildApp + # Comma-separated artifact names that must exist + REQUIRED_ARTIFACTS: frontend,webapi + # How many recent successful runs to scan + SCAN_LIMIT: "100" + steps: - - name: Checkout (for tag context only) + - name: Checkout uses: https://github.com/actions/checkout@v4 - - name: Install tools + - name: Use Node.js 20 + uses: https://github.com/actions/setup-node@v4 + with: + node-version: 20 + + - name: Install unzip (for extraction) run: | sudo apt-get update - sudo apt-get install -y jq zip + sudo apt-get install -y unzip - # 1) Find latest successful run of "BuildApp" via REST API (/api/v1 ... /actions/tasks) - - name: Find latest successful BuildApp run - id: find_run - shell: bash + - name: Resolve latest run that exposes required artifacts + id: resolve + env: + GITEA_PAT: ${{ secrets.GITEA_PAT }} run: | - set -euo pipefail + node .gitea/scripts/getLatestRunWithArtifacts.js + echo "Resolved run_id: $(cat .gitea/.cache/run_id)" - # NOTE: /api/v1 is the correct REST base for Gitea. - RESP="$(curl -fsSL \ - -H "Authorization: token ${{ secrets.GITEA_PAT }}" \ - "$GITEA_BASE_URL/api/v1/repos/$OWNER/$REPO/actions/tasks?limit=100")" - - # Try to match by various fields (workflow.name in newer versions, fallbacks otherwise) - RUN_JSON="$(jq -r --arg W "$WORKFLOW_NAME" ' - .workflow_runs - | map( - select( - (.status=="completed") and - (.conclusion=="success") and - ( - (.workflow.name? // .workflow_name? // .display_title? // "") == $W - ) - ) - ) - | sort_by(.run_number // .id) - | reverse - | .[0] - ' <<< "$RESP")" - - if [[ -z "$RUN_JSON" || "$RUN_JSON" == "null" ]]; then - echo "No successful run found for workflow name: $WORKFLOW_NAME" - echo "Available runs (debug):" - echo "$RESP" | jq -r '.workflow_runs[] | {id, status, conclusion, display_title, workflow_name: .workflow_name, wname: .workflow.name}' - exit 1 - fi - - RUN_ID="$(jq -r '.id // .run_id' <<< "$RUN_JSON")" - HEAD_SHA="$(jq -r '.head_sha // .head_commit?.id // empty' <<< "$RUN_JSON")" - - echo "run_id=$RUN_ID" >> "$GITHUB_OUTPUT" - echo "head_sha=$HEAD_SHA" >> "$GITHUB_OUTPUT" - echo "Latest successful $WORKFLOW_NAME run: $RUN_ID ($HEAD_SHA)" - - # 2) Download artifacts using GUI-style URLs - # These URLs serve ZIPs directly. Authorization via token MAY or MAY NOT work depending on instance config. - # If it fails with 302/403, consider using a community download action as a fallback. - - name: Download 'frontend' artifact (GUI URL) - shell: bash + - name: Download frontend artifact + env: + GITEA_PAT: ${{ secrets.GITEA_PAT }} + ARTIFACT_NAME: frontend + RUN_ID: ${{ steps.resolve.outputs.run_id }} + OUTPUT_DIR: artifacts/frontend run: | - set -euo pipefail - mkdir -p artifacts/frontend - # -L to follow redirects; --fail-with-body to fail on non-2xx - curl -LfS --fail-with-body \ - -H "Authorization: token ${{ secrets.GITEA_PAT }}" \ - "$GITEA_BASE_URL/$OWNER/$REPO/actions/runs/${{ steps.find_run.outputs.run_id }}/artifacts/frontend" \ - -o frontend.zip - unzip -q frontend.zip -d artifacts/frontend - rm -f frontend.zip + node .gitea/scripts/downloadArtifactByName.js - - name: Download 'webapi' artifact (GUI URL) - shell: bash + - name: Download webapi artifact + env: + GITEA_PAT: ${{ secrets.GITEA_PAT }} + ARTIFACT_NAME: webapi + RUN_ID: ${{ steps.resolve.outputs.run_id }} + OUTPUT_DIR: artifacts/webapi run: | - set -euo pipefail - mkdir -p artifacts/webapi - curl -LfS --fail-with-body \ - -H "Authorization: token ${{ secrets.GITEA_PAT }}" \ - "$GITEA_BASE_URL/$OWNER/$REPO/actions/runs/${{ steps.find_run.outputs.run_id }}/artifacts/webapi" \ - -o webapi.zip - unzip -q webapi.zip -d artifacts/webapi - rm -f webapi.zip + node .gitea/scripts/downloadArtifactByName.js - name: Show artifact structure run: | @@ -99,32 +62,4 @@ jobs: echo "::endgroup::" echo "::group::webapi" ls -laR artifacts/webapi || true - echo "::endgroup::" - - # 3) Package artifacts as ZIPs for the release assets - - name: Package artifacts as ZIPs - run: | - mkdir -p build - (cd artifacts/frontend && zip -rq ../../build/frontend.zip .) - (cd artifacts/webapi && zip -rq ../../build/webapi.zip .) - ls -la build - - # 4) Auto-generate tag/title and create the release on Gitea - - name: Generate tag and title - id: meta - run: | - TAG="v$(date -u +%Y%m%d-%H%M)-run${{ steps.find_run.outputs.run_id }}" - TITLE="Release ${TAG}" - echo "tag=$TAG" >> "$GITHUB_OUTPUT" - echo "title=$TITLE" >> "$GITHUB_OUTPUT" - - - name: Create Gitea release with assets - uses: https://gitea.com/actions/release-action@main - with: - api_key: ${{ secrets.GITEA_PAT }} - tag_name: ${{ steps.meta.outputs.tag }} - name: ${{ steps.meta.outputs.title }} - body: "Automated release from latest successful **${{ env.WORKFLOW_NAME }}** run (`run_id=${{ steps.find_run.outputs.run_id }}`, `commit=${{ steps.find_run.outputs.head_sha }}`)." - files: | - build/frontend.zip - build/webapi.zip \ No newline at end of file + echo "::endgroup::" \ No newline at end of file diff --git a/.gitea/workflows/scripts/downloadArtifactByName.js b/.gitea/workflows/scripts/downloadArtifactByName.js new file mode 100644 index 0000000..8d71f49 --- /dev/null +++ b/.gitea/workflows/scripts/downloadArtifactByName.js @@ -0,0 +1,67 @@ +// .gitea/scripts/downloadArtifactByName.js +// Purpose: Download and extract a single artifact by name from a given run. +// Env inputs: +// GITEA_BASE_URL, OWNER, REPO, GITEA_PAT +// RUN_ID -> numeric/string +// ARTIFACT_NAME -> e.g. "frontend" or "webapi" +// OUTPUT_DIR -> e.g. "artifacts/frontend" + +const fs = require("fs"); +const path = require("path"); +const { execSync } = require("child_process"); + +(async () => { + const BASE = process.env.GITEA_BASE_URL; + const OWNER = process.env.OWNER; + const REPO = process.env.REPO; + const TOKEN = process.env.GITEA_PAT; + const RUN_ID = process.env.RUN_ID; + const NAME = process.env.ARTIFACT_NAME; + const OUT_DIR = process.env.OUTPUT_DIR || path.join("artifacts", NAME || ""); + + if (!BASE || !OWNER || !REPO) { + console.error("Missing one of: GITEA_BASE_URL, OWNER, REPO"); + process.exit(1); + } + if (!TOKEN) { + console.error("Missing GITEA_PAT"); + process.exit(1); + } + if (!RUN_ID || !NAME) { + console.error("Missing RUN_ID or ARTIFACT_NAME"); + process.exit(1); + } + + const url = `${BASE}/${OWNER}/${REPO}/actions/runs/${RUN_ID}/artifacts/${encodeURIComponent(NAME)}`; + const zipPath = path.join(process.cwd(), `${NAME}.zip`); + fs.mkdirSync(OUT_DIR, { recursive: true }); + + console.log(`Downloading artifact "${NAME}" from run ${RUN_ID}`); + console.log(`GET ${url}`); + + const res = await fetch(url, { + method: "GET", + redirect: "follow", + headers: { Authorization: `token ${TOKEN}` } + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + console.error(`Download failed: ${res.status} ${res.statusText}\n${text}`); + process.exit(1); + } + + const buf = Buffer.from(await res.arrayBuffer()); + fs.writeFileSync(zipPath, buf); + + console.log(`Saved ZIP -> ${zipPath}`); + console.log(`Extracting to -> ${OUT_DIR}`); + + execSync(`unzip -o "${zipPath}" -d "${OUT_DIR}"`, { stdio: "inherit" }); + fs.unlinkSync(zipPath); + + console.log("Done."); +})().catch(err => { + console.error(err.stack || err.message || String(err)); + process.exit(1); +}); \ No newline at end of file diff --git a/.gitea/workflows/scripts/getLatestRunWithArtifacts.js b/.gitea/workflows/scripts/getLatestRunWithArtifacts.js new file mode 100644 index 0000000..b79ff87 --- /dev/null +++ b/.gitea/workflows/scripts/getLatestRunWithArtifacts.js @@ -0,0 +1,126 @@ +// .gitea/scripts/getLatestRunWithArtifacts.js +// Purpose: Find latest successful run that exposes all REQUIRED_ARTIFACTS via GUI URLs. +// Outputs: sets `run_id` to GITHUB_OUTPUT and writes .gitea/.cache/run_id file. + +const fs = require("fs"); +const path = require("path"); + +(async () => { + // --- Config from environment --- + const BASE = process.env.GITEA_BASE_URL; // e.g. https://code.bim-it.pl + const OWNER = process.env.OWNER; // e.g. mz + const REPO = process.env.REPO; // e.g. DiunaBI + const TOKEN = process.env.GITEA_PAT; // PAT + const SCAN_LIMIT = Number(process.env.SCAN_LIMIT || "100"); + const REQUIRED_ARTIFACTS = (process.env.REQUIRED_ARTIFACTS || "frontend,webapi") + .split(",") + .map(s => s.trim()) + .filter(Boolean); + + if (!BASE || !OWNER || !REPO) { + console.error("Missing one of: GITEA_BASE_URL, OWNER, REPO"); + process.exit(1); + } + if (!TOKEN) { + console.error("Missing GITEA_PAT"); + process.exit(1); + } + + // Ensure cache dir exists + const cacheDir = path.join(".gitea", ".cache"); + fs.mkdirSync(cacheDir, { recursive: true }); + + // Helpers + const api = async (url) => { + const res = await fetch(url, { + headers: { Authorization: `token ${TOKEN}` } + }); + if (!res.ok) { + const text = await res.text().catch(() => ""); + throw new Error(`API ${res.status} ${res.statusText} for ${url}\n${text}`); + } + return res.json(); + }; + + const headOk = async (url) => { + // Try HEAD first; some instances may require GET for redirects + let res = await fetch(url, { + method: "HEAD", + redirect: "follow", + headers: { Authorization: `token ${TOKEN}` } + }); + if (res.ok) return true; + + // Fallback to GET (no download) just to test availability + res = await fetch(url, { + method: "GET", + redirect: "manual", + headers: { Authorization: `token ${TOKEN}` } + }); + // Accept 200 OK, or 3xx redirect to a signed download URL + return res.status >= 200 && res.status < 400; + }; + + // 1) Get recent workflow runs (a.k.a. tasks) via REST + const listUrl = `${BASE}/api/v1/repos/${OWNER}/${REPO}/actions/tasks?limit=${SCAN_LIMIT}`; + const resp = await api(listUrl); + + // 2) Build candidate list: only status == "success", newest first by id + const runs = Array.isArray(resp.workflow_runs) ? resp.workflow_runs : []; + const candidates = runs + .filter(r => r && r.status === "success") + .sort((a, b) => (b.id ?? 0) - (a.id ?? 0)); + + if (!candidates.length) { + console.error("No successful runs found."); + process.exit(1); + } + + console.log(`Scanning ${candidates.length} successful runs for artifacts: ${REQUIRED_ARTIFACTS.join(", ")}`); + + // 3) Find the first run that exposes all required artifacts via GUI URLs + let picked = null; + for (const r of candidates) { + const runId = r.id; + const urls = REQUIRED_ARTIFACTS.map(name => + `${BASE}/${OWNER}/${REPO}/actions/runs/${runId}/artifacts/${encodeURIComponent(name)}` + ); + + let allPresent = true; + for (const u of urls) { + const ok = await headOk(u).catch(() => false); + if (!ok) { + allPresent = false; + console.log(`Run ${runId}: artifact not accessible -> ${u}`); + break; + } + } + if (allPresent) { + picked = { id: runId }; + console.log(`Picked run_id=${runId}`); + break; + } + } + + if (!picked) { + console.error("No run exposes all required artifacts. Consider increasing SCAN_LIMIT or verify artifact names."); + process.exit(1); + } + + // 4) Write outputs + const runIdStr = String(picked.id); + // Write to cache (handy for debugging) + fs.writeFileSync(path.join(cacheDir, "run_id"), runIdStr, "utf8"); + + // Export as GitHub-style output (supported by Gitea runners) + const outFile = process.env.GITHUB_OUTPUT; + if (outFile) { + fs.appendFileSync(outFile, `run_id=${runIdStr}\n`); + } else { + // Fallback: also print for visibility + console.log(`::set-output name=run_id::${runIdStr}`); + } +})().catch(err => { + console.error(err.stack || err.message || String(err)); + process.exit(1); +}); \ No newline at end of file