ref #1185 fixed #984
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Template configuration for Convertigo project CI build on GitHub Actions | |
| # Please consult the GitHub documentation for details about settings | |
| # https://docs.github.com/en/actions | |
| # | |
| # This sample assumes you have declared the following Encrypted Secrets | |
| # https://docs.github.com/en/actions/reference/encrypted-secrets | |
| # | |
| # C8O_SERVER: Convertigo server endpoint, where the built mobile application will connect | |
| # and the backend project will be deployed, like https://<myhost>/convertigo | |
| # C8O_USER: Convertigo server admin or a user with role PROJECTS_CONFIG, used for the deploiment | |
| # C8O_PASSWORD: Convertigo server password for the C8O_USER | |
| # | |
| # C8O_SERVER_PROD: override C8O_SERVER for tags | |
| # C8O_USER_PROD: override C8O_USER for tags | |
| # C8O_PASSWORD_PROD: override C8O_PASSWORD for tags | |
| # | |
| # Discover all tasks and -Pconvertigo options in your project build.gradle file. | |
| name: No code studio CI | |
| on: | |
| push: | |
| tags: | |
| - '**' | |
| jobs: | |
| build_and_deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| ssh-key: ${{ secrets.SSH_KEY }} | |
| - name: Setup Java | |
| uses: actions/setup-java@v4 | |
| with: | |
| java-version: '17' | |
| distribution: 'temurin' | |
| - name: Cache Gradle Dependencies | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ~/.gradle/caches | |
| ~/.gradle/wrapper | |
| key: ${{ runner.os }}-gradle-caches-v1-${{ hashFiles('**/*.gradle', '**/gradle/wrapper/gradle-wrapper.properties') }} | |
| restore-keys: ${{ runner.os }}-gradle-caches-v1- | |
| - name: Cache Node Modules | |
| uses: actions/cache@v4 | |
| with: | |
| path: | | |
| ${{ github.workspace }}/_private/ionic/node_modules | |
| ${{ github.workspace }}/_private/ionic/.angular | |
| /home/runner/convertigo/nodes | |
| key: ${{ runner.os }}-build-node-${{ hashFiles('**/package.json', '**/_private/ionic/version.json') }} | |
| restore-keys: ${{ runner.os }}-build-node- | |
| - name: Convertigo generate application code | |
| env: | |
| C8O_SERVER: ${{secrets.testEndpoint}} | |
| run: | | |
| echo "Generate Mobile Builder App" && \ | |
| sh gradlew --stacktrace --info generateMobileBuilder export -x compileMobileBuilder \ | |
| -Pconvertigo.load.mobileApplicationEndpoint=$C8O_SERVER | |
| - name: Build and deploy based on branch or tag | |
| env: | |
| C8O_SERVER: ${{secrets.testEndpoint}} | |
| C8O_USER: ${{ secrets.testUserAdmin }} | |
| C8O_PASSWORD: ${{ secrets.testUserPassword }} | |
| run: | | |
| echo "Deploying to test server $C8O_SERVER"; | |
| sh gradlew --stacktrace --info car deploy exportDependencies -x generateMobileBuilder \ | |
| -Pconvertigo.deploy.server=$C8O_SERVER \ | |
| -Pconvertigo.deploy.user=$C8O_USER \ | |
| -Pconvertigo.deploy.password=$C8O_PASSWORD | |
| - name: Create "c8oforms_standalone" dockerized version | |
| run: | | |
| OLD_README=$(cat c8oforms_standalone/README.md) | |
| curl -sL https://github.com/convertigo/docker/archive/refs/tags/c8oforms-2.1.0.tar.gz | tar xvz --strip-components=1 -C c8oforms_standalone | |
| NEW_README=$(cat c8oforms_standalone/README.md) | |
| echo -e "${OLD_README}\n\n${NEW_README}" > c8oforms_standalone/README.md | |
| sed -i -e "s,HTTPD_ENABLE=0,HTTPD_ENABLE=1," -e "s,BASEROW_ENABLE=0,BASEROW_ENABLE=1," -e "s,HTTPD_ROOT_URL=/convertigo/,HTTPD_ROOT_URL=/convertigo/projects/C8Oforms/DisplayObjects/mobile/index.html," c8oforms_standalone/.env | |
| mkdir -p c8oforms_standalone/data/workspace/projects/BaserowIntegration | |
| find build -name "*.car" -exec unzip {} -d c8oforms_standalone/data/workspace/projects \; | |
| find c8oforms_standalone/data/workspace/projects -mindepth 2 -maxdepth 2 -type f -name "c8oProject.yaml" -exec sed -i 's/convertigo: 8\.4\./convertigo: 8.3./' {} + | |
| curl -sL https://github.com/convertigo/c8oprj-baserowintegration/archive/refs/tags/c8oforms-2.1.0.tar.gz | tar xvz --strip-components=1 -C c8oforms_standalone/data/workspace/projects/BaserowIntegration | |
| tar -czvf c8oforms_standalone.tar.gz c8oforms_standalone | |
| - name: Create "no_code_studio_and_dependencies.zip" (.car per project; keep folder root) | |
| run: | | |
| set -euo pipefail | |
| ROOT="c8oforms_standalone/data/workspace/projects" | |
| if [ ! -d "$ROOT" ]; then | |
| echo "Directory not found: $ROOT" | |
| exit 1 | |
| fi | |
| cd "$ROOT" | |
| shopt -s nullglob | |
| made_any=0 | |
| # Un .car par dossier projet (le .car garde le dossier à la racine) | |
| for p in */ ; do | |
| name="${p%/}" | |
| if [ -f "${name}/c8oProject.yaml" ] || [ -f "${name}/c8oproject.yaml" ]; then | |
| echo "Building ${name}.car (folder preserved)" | |
| zip -qr "${name}.zip" "${name}" | |
| mv -f "${name}.zip" "${name}.car" | |
| made_any=1 | |
| fi | |
| done | |
| # Regroupe tous les .car dans un ZIP à la racine du repo | |
| dest="$GITHUB_WORKSPACE/no_code_studio_and_dependencies.zip" | |
| if [ "$made_any" -eq 1 ]; then | |
| zip -q -r "$dest" *.car | |
| else | |
| echo "No .car produced, creating empty zip to keep pipeline consistent." | |
| mkdir -p ._empty && zip -qr "$dest" ._empty && rm -rf ._empty | |
| fi | |
| - name: Save no_code_studio_and_dependencies | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: no_code_studio_and_dependencies.zip | |
| path: no_code_studio_and_dependencies.zip | |
| - name: Save application project | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: project | |
| path: build/C8Oforms.car | |
| - name: Save application c8oforms_standalone | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: c8oforms_standalone.tar.gz | |
| path: c8oforms_standalone.tar.gz | |
| deps_report: | |
| runs-on: ubuntu-latest | |
| needs: build_and_deploy | |
| steps: | |
| - name: Checkout (full history with tags) | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| fetch-tags: true | |
| ref: ${{ github.ref }} | |
| - name: Locate c8oproject file at current tag (non-fatal) | |
| id: locate_current_file | |
| shell: bash | |
| run: | | |
| set -e | |
| CURRENT_TAG="${GITHUB_REF#refs/tags/}" | |
| echo "current_tag=$CURRENT_TAG" >> $GITHUB_OUTPUT | |
| FILE_PATH=$(git ls-tree -r --name-only "$CURRENT_TAG" | grep -i -E '(^|/)(c8oproject\.yaml|c8oProject\.yaml)$' | head -n1 || true) | |
| echo "file_path=$FILE_PATH" >> $GITHUB_OUTPUT | |
| - name: Find previous tag (semantic) | |
| id: prev_tag | |
| shell: bash | |
| env: | |
| CURRENT_TAG: ${{ steps.locate_current_file.outputs.current_tag }} | |
| run: | | |
| set -euo pipefail | |
| echo "CURRENT_TAG=${CURRENT_TAG}" | |
| python3 - <<'PY' | |
| import os, re, subprocess, sys | |
| def parse_tag(t): | |
| # Ex: 2.1.3 , 2.1.3-beta2 , 2.1.3-rc1 , 2.1.3-alpha3 | |
| m = re.fullmatch(r'(\d+)\.(\d+)\.(\d+)(?:-([A-Za-z]+)(\d+))?', t) | |
| if not m: | |
| return None | |
| major, minor, patch = map(int, m.group(1,2,3)) | |
| pre_name = (m.group(4) or "").lower() | |
| pre_num = int(m.group(5)) if m.group(5) else 0 | |
| order = {"alpha": 0, "beta": 1, "rc": 2, "": 3} # alpha < beta < rc < final | |
| return (major, minor, patch, order.get(pre_name, -1), pre_num) | |
| def get_current_tag(): | |
| cur = os.environ.get("CURRENT_TAG", "") | |
| if cur: | |
| return cur | |
| # Fallback: on essaie de déduire depuis git | |
| try: | |
| cur = subprocess.check_output(["git", "describe", "--tags", "--exact-match"], text=True).strip() | |
| return cur | |
| except Exception: | |
| return "" | |
| current = get_current_tag() | |
| if not current: | |
| # Rien à faire, on renvoie un vide (et on n'échoue pas le job) | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write("previous_tag=\n") | |
| sys.exit(0) | |
| tags = subprocess.check_output(["git", "tag", "--list"], text=True).splitlines() | |
| parsed = [] | |
| for t in tags: | |
| p = parse_tag(t) | |
| if p is not None: | |
| parsed.append((t, p)) | |
| # Trie croissant sémantique | |
| parsed.sort(key=lambda x: x[1]) | |
| # Trouve l’index du tag courant et prend l’immédiat précédent | |
| idx = next((i for i,(t,p) in enumerate(parsed) if t == current), None) | |
| prev_tag = parsed[idx-1][0] if idx is not None and idx > 0 else "" | |
| with open(os.environ["GITHUB_OUTPUT"], "a") as f: | |
| f.write(f"previous_tag={prev_tag}\n") | |
| PY | |
| - name: Generate dependency report (Markdown + JSON) — never fail | |
| shell: bash | |
| continue-on-error: true | |
| env: | |
| CURRENT_TAG: ${{ steps.locate_current_file.outputs.current_tag }} | |
| PREV_TAG: ${{ steps.prev_tag.outputs.previous_tag }} | |
| FILE_PATH: ${{ steps.locate_current_file.outputs.file_path }} | |
| run: | | |
| set -euo pipefail | |
| mkdir -p dist .github/scripts | |
| cat > .github/scripts/gen_deps_report.py << 'PY' | |
| import os, re, json, subprocess | |
| from collections import OrderedDict | |
| CUR = os.environ.get("CURRENT_TAG","").strip() | |
| PRE = os.environ.get("PREV_TAG","").strip() | |
| HINT = os.environ.get("FILE_PATH","").strip() | |
| def git_ls_yaml_at(ref): | |
| try: | |
| out = subprocess.check_output(["git","ls-tree","-r","--name-only",ref], text=True) | |
| return [l for l in out.splitlines() if l.lower().endswith((".yaml",".yml"))] | |
| except Exception: | |
| return [] | |
| def git_show(ref, path): | |
| if not ref or not path: | |
| return "" | |
| try: | |
| return subprocess.check_output(["git","show",f"{ref}:{path}"], text=True) | |
| except Exception: | |
| return "" | |
| def find_c8o_path_at(ref, hint): | |
| if ref and hint: | |
| txt = git_show(ref, hint) | |
| if txt: | |
| return hint, txt | |
| for p in git_ls_yaml_at(ref): | |
| low = p.lower() | |
| if "c8oproject" in low: | |
| txt = git_show(ref, p) | |
| if txt: | |
| return p, txt | |
| return "", "" | |
| def parse_core(y): | |
| m = re.search(r'^[^\S\r\n]*[↑]*convertigo:\s*([^\s]+)', y, re.MULTILINE) | |
| return m.group(1).strip() if m else None | |
| def parse_refs(y): | |
| """Scan line-by-line to capture every references.ProjectSchemaReference block, | |
| then read the first `projectName:` line inside.""" | |
| deps = OrderedDict() | |
| if not y: return deps | |
| lines = y.splitlines() | |
| in_block = False | |
| captured = False | |
| for line in lines: | |
| head = line.lstrip() | |
| # Start of a new object header | |
| if head.startswith("↓") and "[" in head: | |
| in_block = ("references.ProjectSchemaReference" in head) | |
| captured = False | |
| continue | |
| # Safety: header without the arrow (rare) | |
| if "references.ProjectSchemaReference" in head and "[" in head and not head.startswith("↓"): | |
| in_block = True | |
| captured = False | |
| continue | |
| if in_block and not captured and "projectName:" in line: | |
| m = re.search(r'projectName:\s*([^\s=:#]+)\s*=\s*([^\s]+)', line.strip()) | |
| if m: | |
| name = m.group(1).strip() | |
| rhs = m.group(2).strip() | |
| # version detection | |
| ver = "unknown" | |
| m_rel = re.search(r'/releases/download/([^/]+)/', rhs) | |
| if m_rel: | |
| ver = m_rel.group(1) | |
| else: | |
| m_br = re.search(r':branch=([^:]+)', rhs) | |
| if m_br: | |
| ver = m_br.group(1) | |
| elif rhs.endswith('.git') or '.git:' in rhs: | |
| ver = "main" | |
| deps[name] = {"version": ver, "source": rhs} | |
| captured = True | |
| return deps | |
| # Load current and previous YAML (path may differ between tags) | |
| cur_path, cur_yaml = find_c8o_path_at(CUR, HINT) | |
| prev_path, prev_yaml = ("","") | |
| if PRE: | |
| prev_path, prev_yaml = find_c8o_path_at(PRE, cur_path or HINT) | |
| cur_core = parse_core(cur_yaml) | |
| prev_core = parse_core(prev_yaml) if prev_yaml else None | |
| cur_deps = parse_refs(cur_yaml) | |
| prev_deps = parse_refs(prev_yaml) | |
| def diff(cur, prev): | |
| added, removed, changed, same = [], [], [], [] | |
| ck, pk = set(cur), set(prev) | |
| for k in sorted(ck - pk): added.append(k) | |
| for k in sorted(pk - ck): removed.append(k) | |
| for k in sorted(ck & pk): | |
| if cur[k]["version"] != prev[k]["version"]: | |
| changed.append(k) | |
| else: | |
| same.append(k) | |
| return added, removed, changed, same | |
| added, removed, changed, same = diff(cur_deps, prev_deps) | |
| # ---------- Grouping ---------- | |
| def is_build_dep(meta): | |
| # Build-only if URL contains "-ui-ngx" (case-insensitive) | |
| return "-ui-ngx" in meta["source"].lower() | |
| server_names = sorted([n for n,m in cur_deps.items() if not is_build_dep(m)], key=str.lower) | |
| build_names = sorted([n for n,m in cur_deps.items() if is_build_dep(m)], key=str.lower) | |
| # ---------- Markdown ---------- | |
| lines = [] | |
| lines.append("# Dependencies report\n") | |
| lines.append(f"- **Current tag:** `{CUR or 'unknown'}`") | |
| if PRE: lines.append(f"- **Previous tag:** `{PRE}`") | |
| if cur_core: | |
| ln = f"- **Convertigo core:** `{cur_core}`" | |
| if prev_core and prev_core != cur_core: | |
| ln += f" → **changed** (was `{prev_core}`)" | |
| lines.append(ln) | |
| lines.append("\n## Summary of changes") | |
| if not cur_deps: | |
| lines.append("- ⚠️ No `c8oproject.yaml` found at current tag or empty file.") | |
| else: | |
| if changed: lines.append(f"- 🔄 **Updated** ({len(changed)}): " + ", ".join(f"`{k}`" for k in changed)) | |
| if added: lines.append(f"- ➕ **Added** ({len(added)}): " + ", ".join(f"`{k}`" for k in added)) | |
| if removed: lines.append(f"- ➖ **Removed** ({len(removed)}): " + ", ".join(f"`{k}`" for k in removed)) | |
| if not (changed or added or removed): | |
| lines.append("- No changes vs previous tag.") | |
| def render_table(title, names): | |
| if not names: | |
| return [f"\n## {title}\n\n_No dependencies detected._\n"] | |
| out = [f"\n## {title}\n"] | |
| out.append("") | |
| out.append("| Library | Version | Source | Diff vs previous |") | |
| out.append("|---|---:|---|---|") | |
| for name in names: | |
| meta = cur_deps[name] | |
| ver, raw = meta["version"], meta["source"] | |
| if name in added: diffv = "➕ Added" | |
| elif name in changed: | |
| pv = prev_deps.get(name, {}).get("version", "n/a") | |
| diffv = f"🔄 {pv} → **{ver}**" | |
| else: | |
| diffv = "—" | |
| out.append(f"| `{name}` | `{ver}` | `{raw}` | {diffv} |") | |
| return out | |
| # Server dependencies first, then build-time | |
| lines += render_table("Server dependencies (required at runtime)", server_names) | |
| lines += render_table("Build-time dependencies (required to compile the project)", build_names) | |
| # ---------- Outputs ---------- | |
| os.makedirs("dist", exist_ok=True) | |
| with open("dist/dependencies_report.md","w") as f: | |
| f.write("\n".join(lines)) | |
| # Keep simple “recommended versions” list (all deps), still useful for quick copy/paste | |
| with open("dist/dependencies_versions.md","w") as f: | |
| f.write("# Recommended versions\n\n" + "\n".join( | |
| f"- `{n}` = `{m['version']}`" for n,m in cur_deps.items() | |
| )) | |
| with open("dist/dependencies_report.json","w") as f: | |
| json.dump({ | |
| "current_tag": CUR, | |
| "previous_tag": PRE or None, | |
| "convertigo_core": {"current": cur_core, "previous": prev_core}, | |
| "groups": { | |
| "server": server_names, | |
| "build": build_names | |
| }, | |
| "dependencies": cur_deps, | |
| "diff": {"added": added, "removed": removed, "changed": changed, "same": same} | |
| }, f, indent=2) | |
| PY | |
| python3 .github/scripts/gen_deps_report.py || true | |
| # Guarantee artifacts exist even if parsing fails | |
| [[ -f dist/dependencies_report.md ]] || echo -e "# Dependencies report\n\n(Empty fallback)\n" > dist/dependencies_report.md | |
| [[ -f dist/dependencies_versions.md ]] || echo -e "# Recommended versions\n\n(Empty fallback)\n" > dist/dependencies_versions.md | |
| [[ -f dist/dependencies_report.json ]] || echo '{}' > dist/dependencies_report.json | |
| - name: Upload dependency report artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: dependencies_report | |
| path: | | |
| dist/dependencies_report.md | |
| dist/dependencies_versions.md | |
| dist/dependencies_report.json | |
| - name: Append report to Job Summary | |
| if: ${{ hashFiles('dist/dependencies_report.md') != '' }} | |
| run: | | |
| echo "## Dependencies report" >> "$GITHUB_STEP_SUMMARY" | |
| cat dist/dependencies_report.md >> "$GITHUB_STEP_SUMMARY" | |
| release: | |
| needs: [build_and_deploy, deps_report] | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download CAR File | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: project | |
| - name: Download c8oforms_standalone.tar.gz File | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: c8oforms_standalone.tar.gz | |
| - name: Download no_code_studio_and_dependencies.zip File | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: no_code_studio_and_dependencies.zip | |
| - name: Download dependency report | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: dependencies_report | |
| path: dist | |
| - name: Deploy to GitHub Release (with dependencies report) | |
| uses: softprops/action-gh-release@v2 | |
| with: | |
| files: | | |
| *.car | |
| c8oforms_standalone.tar.gz | |
| no_code_studio_and_dependencies.zip | |
| tag_name: ${{ github.ref_name }} | |
| draft: ${{ contains( github.ref, 'beta' ) != true }} | |
| prerelease: ${{ contains( github.ref, 'beta' )}} | |
| generate_release_notes: true | |
| body_path: dist/dependencies_report.md | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| close_and_set_issues_to_be_tested: | |
| needs: build_and_deploy | |
| runs-on: ubuntu-latest | |
| permissions: | |
| issues: write | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v3 | |
| with: | |
| fetch-tags: true | |
| ref: ${{ github.ref }} | |
| - name: Get tag message | |
| id: get_tag_message | |
| run: | | |
| TAG_NAME=${GITHUB_REF#refs/tags/} | |
| echo "Tag name: $TAG_NAME" | |
| TAG_MESSAGE=$(git for-each-ref refs/tags/$TAG_NAME --format='%(contents)') | |
| echo "Tag message: $TAG_MESSAGE" | |
| echo "tag_name=$TAG_NAME" >> $GITHUB_OUTPUT | |
| echo "tag_message<<EOF" >> $GITHUB_OUTPUT | |
| echo "$TAG_MESSAGE" >> $GITHUB_OUTPUT | |
| echo "EOF" >> $GITHUB_OUTPUT | |
| shell: bash | |
| - name: Extract issue numbers | |
| id: extract_issues | |
| run: | | |
| TAG_MESSAGE="${{ steps.get_tag_message.outputs.tag_message }}" | |
| echo "Tag message: $TAG_MESSAGE" | |
| RAW_ISSUE_NUMBERS=$(echo "$TAG_MESSAGE" | grep -oE '#[0-9]+' || true) | |
| ISSUE_NUMBERS=$(echo "$RAW_ISSUE_NUMBERS" | tr -d '#' | tr '\n' ' ') | |
| echo "Found issue numbers: $ISSUE_NUMBERS" | |
| echo "issue_numbers=$ISSUE_NUMBERS" >> $GITHUB_OUTPUT | |
| shell: bash | |
| - name: Post comment on issues | |
| if: ${{ steps.extract_issues.outputs.issue_numbers != '' }} | |
| uses: actions/github-script@v6 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const tagName = "${{ steps.get_tag_message.outputs.tag_name }}"; | |
| const issueNumbers = "${{ steps.extract_issues.outputs.issue_numbers }}".split(' '); | |
| const repoOwner = context.repo.owner; | |
| const repoName = context.repo.repo; | |
| const tagUrl = `https://github.com/${repoOwner}/${repoName}/tree/refs/tags/${tagName}`; | |
| const commentBody = `To be tested in [${tagName}](${tagUrl})`; | |
| for (const issueNumber of issueNumbers) { | |
| if (issueNumber) { | |
| console.log(`Posting comment to issue #${issueNumber}`); | |
| await github.rest.issues.createComment({ | |
| owner: repoOwner, | |
| repo: repoName, | |
| issue_number: parseInt(issueNumber), | |
| body: commentBody | |
| }); | |
| console.log(`Closing issue #${issueNumber}`); | |
| await github.rest.issues.update({ | |
| owner: repoOwner, | |
| repo: repoName, | |
| issue_number: parseInt(issueNumber), | |
| state: 'closed' | |
| }); | |
| } | |
| } |