# KiCad CI — reusable Gitea Actions workflow. # # Ported from cookietronics/keeb@dev, which is where this design was # actually debugged into working. Callers should be a ten-line stub: # # jobs: # kicad: # uses: public/kicad-ci/.gitea/workflows/kibot.yml@main # with: # project_name: m120101-honey_harvester_mainboard # secrets: # ASTABLE_URL: ${{ secrets.ASTABLE_URL }} # ASTABLE_API_TOKEN: ${{ secrets.ASTABLE_API_TOKEN }} # # Architecture note (different from the GitLab side, on purpose): # We do NOT use `container:` on jobs. Gitea Actions runs every step # (including actions/checkout's Node-based post step) inside the job # container — but the inti-cmnb/kicad*_auto images don't ship Node, # so checkout's cleanup dies with exit 127 and the whole job fails # regardless of what kibot did. Workaround: jobs run on the regular # act-latest runner image (which has Node) and shell out to # `docker run` for each kibot invocation. KiBot still runs in the # pinned image; only the wrapper plumbing lives on the host. # # Required secrets, passed by the caller (set them at org level): # ASTABLE_URL e.g. https://astable.micromelon.dev # ASTABLE_API_TOKEN minted at /manage/api-tokens (ast_…) # # Boards predating KiCad 10 may fail to parse under the default image; # pin an older one via the kibot_image input if a board needs it. name: KiBot CI (reusable) on: workflow_call: inputs: project_name: description: "Project directory name — also the partIpn for Astable" required: true type: string kibot_image: description: "KiBot container image to run KiCad tooling in" required: false type: string default: "ghcr.io/inti-cmnb/kicad10_auto:latest" secrets: ASTABLE_URL: required: true ASTABLE_API_TOKEN: required: true env: PROJECT_NAME: ${{ inputs.project_name }} KIBOT_IMAGE: ${{ inputs.kibot_image }} jobs: # ─── Preflight ────────────────────────────────────────────────────── extract_version: runs-on: docker-host outputs: version: ${{ steps.derive.outputs.version }} steps: - uses: actions/checkout@v4 - id: derive env: # Inject via env so commit-message / PR-title backticks or # other shell metacharacters don't trigger command substitution # when bash evaluates the script. Direct ${{ … }} interpolation # into the run: body would splice the raw text into bash — # backticks become $(…). Keep this env-var indirection. COMMIT_MESSAGE: ${{ github.event.head_commit.message }} PR_TITLE: ${{ github.event.pull_request.title }} GIT_SHA: ${{ github.sha }} run: | if [ "${{ github.ref }}" = "refs/heads/main" ]; then VERSION=$(printf '%s' "$COMMIT_MESSAGE" | grep -oE 'V[0-9]+(\.[0-9]+)*' | head -1 || true) [ -z "$VERSION" ] && VERSION="${GIT_SHA:0:8}" elif [ "${{ github.event_name }}" = "pull_request" ]; then VERSION=$(printf '%s' "$PR_TITLE" | grep -oE 'V[0-9]+(\.[0-9]+)*' | head -1 || true) [ -z "$VERSION" ] && VERSION="dev" else VERSION="dev" fi echo "version=$VERSION" >> "$GITHUB_OUTPUT" echo "Resolved VERSION=$VERSION" run_erc: runs-on: docker-host needs: extract_version steps: - uses: actions/checkout@v4 with: submodules: recursive - name: KiBot ERC run: | [ -f "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" ] || exit 0 cat > erc.kibot.yaml << 'EOF' kibot: version: 1 preflight: erc: enabled: true warnings_as_errors: false EOF mkdir -p Validation docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ "$KIBOT_IMAGE" \ kibot -e "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" \ -c erc.kibot.yaml -d Validation/ - uses: actions/upload-artifact@v3 if: always() with: name: Validation path: Validation/ retention-days: 7 run_drc: runs-on: docker-host needs: [extract_version, run_erc] env: VERSION: ${{ needs.extract_version.outputs.version }} steps: - uses: actions/checkout@v4 with: submodules: recursive - uses: actions/download-artifact@v3 with: name: Validation path: Validation/ - name: KiBot DRC run: | [ -f "${PROJECT_NAME}/${PROJECT_NAME}.kicad_pcb" ] || exit 0 PART_NUM=$(echo "${PROJECT_NAME}" | cut -d"-" -f1) cat > drc.kibot.yaml << EOF kibot: version: 1 preflight: set_text_variables: - name: 'name' text: '${PART_NUM}' - name: 'rev' text: '${VERSION}' - name: 'rev_pcb' text: '${VERSION}' drc: enabled: true warnings_as_errors: false EOF docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ "$KIBOT_IMAGE" \ kibot -e "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" \ -b "${PROJECT_NAME}/${PROJECT_NAME}.kicad_pcb" \ -c drc.kibot.yaml -d Validation/ - uses: actions/upload-artifact@v3 if: always() with: name: Validation path: Validation/ overwrite: true retention-days: 7 # ─── Fabrication ──────────────────────────────────────────────────── generate_schematic: runs-on: docker-host needs: [extract_version, run_erc, run_drc] env: VERSION: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev_pcb: ${{ needs.extract_version.outputs.version }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - name: KiBot schematic PDF run: | export KIBOT_VAR_name=$(echo "${PROJECT_NAME}" | cut -d"-" -f1) [ -f "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" ] || exit 0 mkdir -p "Fabrication/${PROJECT_NAME}" # default.kibot.yaml imports user.kibot.yaml, which each project # must supply. Mirrors the GitLab pipeline's "Setup user config": # use the project's own kibot.yaml if present, else blank fallback. USER_FILE=".gitlab/configs/blank.kibot.yaml" [ -f "${PROJECT_NAME}/kibot.yaml" ] && USER_FILE="${PROJECT_NAME}/kibot.yaml" cp "$USER_FILE" .gitlab/configs/user.kibot.yaml docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ -e KIBOT_VAR_name -e KIBOT_VAR_rev -e KIBOT_VAR_rev_pcb \ "$KIBOT_IMAGE" \ kibot -e "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" \ -c ".gitlab/configs/sch.kibot.yaml" \ -d "Fabrication/${PROJECT_NAME}" mv "Fabrication/${PROJECT_NAME}"/*.pdf Fabrication/ 2>/dev/null || true - uses: actions/upload-artifact@v3 with: name: Fabrication path: Fabrication/ retention-days: 7 generate_bom: runs-on: docker-host needs: [extract_version, run_erc, run_drc] env: VERSION: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev_pcb: ${{ needs.extract_version.outputs.version }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - name: KiBot BOM run: | export KIBOT_VAR_name=$(echo "${PROJECT_NAME}" | cut -d"-" -f1) [ -f "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" ] || exit 0 mkdir -p "Fabrication/${PROJECT_NAME}" # default.kibot.yaml imports user.kibot.yaml, which each project # must supply. Mirrors the GitLab pipeline's "Setup user config": # use the project's own kibot.yaml if present, else blank fallback. USER_FILE=".gitlab/configs/blank.kibot.yaml" [ -f "${PROJECT_NAME}/kibot.yaml" ] && USER_FILE="${PROJECT_NAME}/kibot.yaml" cp "$USER_FILE" .gitlab/configs/user.kibot.yaml docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ -e KIBOT_VAR_name -e KIBOT_VAR_rev -e KIBOT_VAR_rev_pcb \ "$KIBOT_IMAGE" \ kibot -e "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" \ -c ".gitlab/configs/sch.kibot.yaml" \ -d "Fabrication/${PROJECT_NAME}" mv "Fabrication/${PROJECT_NAME}"/*.csv Fabrication/ 2>/dev/null || true mv "Fabrication/${PROJECT_NAME}"/*.xlsx Fabrication/ 2>/dev/null || true - uses: actions/upload-artifact@v3 with: name: Fabrication-bom path: Fabrication/ retention-days: 7 generate_3d: runs-on: docker-host needs: [extract_version, run_erc, run_drc] env: VERSION: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev_pcb: ${{ needs.extract_version.outputs.version }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - name: KiBot 3D (STEP) run: | export KIBOT_VAR_name=$(echo "${PROJECT_NAME}" | cut -d"-" -f1) [ -f "${PROJECT_NAME}/${PROJECT_NAME}.kicad_pcb" ] || exit 0 mkdir -p "Fabrication/${PROJECT_NAME}" # default.kibot.yaml imports user.kibot.yaml, which each project # must supply. Mirrors the GitLab pipeline's "Setup user config": # use the project's own kibot.yaml if present, else blank fallback. USER_FILE=".gitlab/configs/blank.kibot.yaml" [ -f "${PROJECT_NAME}/kibot.yaml" ] && USER_FILE="${PROJECT_NAME}/kibot.yaml" cp "$USER_FILE" .gitlab/configs/user.kibot.yaml docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ -e KIBOT_VAR_name -e KIBOT_VAR_rev -e KIBOT_VAR_rev_pcb \ "$KIBOT_IMAGE" \ kibot -e "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" \ -c ".gitlab/configs/mech.kibot.yaml" \ -d "Fabrication/${PROJECT_NAME}" mv "Fabrication/${PROJECT_NAME}"/*.step Fabrication/ 2>/dev/null || true - uses: actions/upload-artifact@v3 with: name: Fabrication-3d path: Fabrication/ retention-days: 7 # Main-only jobs — gated by the `if:` so dev branches skip them. generate_gerbers: # Runs on dev and main — dev uploads gerbers to Astable too. runs-on: docker-host needs: [extract_version, run_erc, run_drc] env: VERSION: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev_pcb: ${{ needs.extract_version.outputs.version }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - name: KiBot gerbers run: | export KIBOT_VAR_name=$(echo "${PROJECT_NAME}" | cut -d"-" -f1) [ -f "${PROJECT_NAME}/${PROJECT_NAME}.kicad_pcb" ] || exit 0 mkdir -p "Fabrication/${PROJECT_NAME}" # default.kibot.yaml imports user.kibot.yaml, which each project # must supply. Mirrors the GitLab pipeline's "Setup user config": # use the project's own kibot.yaml if present, else blank fallback. USER_FILE=".gitlab/configs/blank.kibot.yaml" [ -f "${PROJECT_NAME}/kibot.yaml" ] && USER_FILE="${PROJECT_NAME}/kibot.yaml" cp "$USER_FILE" .gitlab/configs/user.kibot.yaml docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ -e KIBOT_VAR_name -e KIBOT_VAR_rev -e KIBOT_VAR_rev_pcb \ "$KIBOT_IMAGE" \ kibot -e "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" \ -c ".gitlab/configs/pcb_main.kibot.yaml" \ -d "Fabrication/${PROJECT_NAME}" mv "Fabrication/${PROJECT_NAME}"/*.zip Fabrication/ 2>/dev/null || true - uses: actions/upload-artifact@v3 with: name: Fabrication-gerbers path: Fabrication/ retention-days: 7 generate_position: runs-on: docker-host needs: [extract_version, run_erc, run_drc] env: VERSION: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev_pcb: ${{ needs.extract_version.outputs.version }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - name: KiBot CPL run: | export KIBOT_VAR_name=$(echo "${PROJECT_NAME}" | cut -d"-" -f1) [ -f "${PROJECT_NAME}/${PROJECT_NAME}.kicad_pcb" ] || exit 0 mkdir -p "Fabrication/${PROJECT_NAME}" # default.kibot.yaml imports user.kibot.yaml, which each project # must supply. Mirrors the GitLab pipeline's "Setup user config": # use the project's own kibot.yaml if present, else blank fallback. USER_FILE=".gitlab/configs/blank.kibot.yaml" [ -f "${PROJECT_NAME}/kibot.yaml" ] && USER_FILE="${PROJECT_NAME}/kibot.yaml" cp "$USER_FILE" .gitlab/configs/user.kibot.yaml docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ -e KIBOT_VAR_name -e KIBOT_VAR_rev -e KIBOT_VAR_rev_pcb \ "$KIBOT_IMAGE" \ kibot -e "${PROJECT_NAME}/${PROJECT_NAME}.kicad_sch" \ -c ".gitlab/configs/pos.kibot.yaml" \ -d "Fabrication/${PROJECT_NAME}" mv "Fabrication/${PROJECT_NAME}"/*cpl*.csv Fabrication/ 2>/dev/null || true - uses: actions/upload-artifact@v3 with: name: Fabrication-position path: Fabrication/ retention-days: 7 generate_panel: runs-on: docker-host needs: [extract_version, run_erc, run_drc] continue-on-error: true # optional per the GitLab side env: VERSION: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev: ${{ needs.extract_version.outputs.version }} KIBOT_VAR_rev_pcb: ${{ needs.extract_version.outputs.version }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - name: KiBot panel run: | export KIBOT_VAR_name=$(echo "${PROJECT_NAME}" | cut -d"-" -f1) PANEL_JSON=$(find "${PROJECT_NAME}" -name "*_panel.json" 2>/dev/null | head -1) if [ -z "$PANEL_JSON" ] || [ ! -f "$PANEL_JSON" ]; then echo "No panel config — skipping" exit 0 fi PANEL_NAME=$(basename "${PANEL_JSON%.json}") mkdir -p "panels/${PANEL_NAME}" # See note in the other jobs: supply user.kibot.yaml for the import. USER_FILE=".gitlab/configs/blank.kibot.yaml" [ -f "${PROJECT_NAME}/kibot.yaml" ] && USER_FILE="${PROJECT_NAME}/kibot.yaml" cp "$USER_FILE" .gitlab/configs/user.kibot.yaml docker run --rm --volumes-from "$(hostname)" -w "$PWD" \ -e KIBOT_VAR_name -e KIBOT_VAR_rev -e KIBOT_VAR_rev_pcb \ -e WORKDIR="$PWD" \ "$KIBOT_IMAGE" \ bash -c " PCB_FILE='${PROJECT_NAME}/${PROJECT_NAME}.kicad_pcb' [ -f .gitlab/.scripts/pre_panel.py ] && python3 .gitlab/.scripts/pre_panel.py \"\$PCB_FILE\" '${PANEL_JSON}' kikit panelize -p '${PANEL_JSON}' \"\$PCB_FILE\" 'panels/${PANEL_NAME}/${PANEL_NAME}.kicad_pcb' cp .gitlab/templates/micromelon_default/micromelon_default.kicad_sch 'panels/${PANEL_NAME}/${PANEL_NAME}.kicad_sch' 2>/dev/null || true cp '${PROJECT_NAME}/fp-lib-table' 'panels/${PANEL_NAME}/' 2>/dev/null || true cp -r '${PROJECT_NAME}/libs' 'panels/${PANEL_NAME}/' 2>/dev/null || true [ -f .gitlab/.scripts/post_panel.py ] && python3 .gitlab/.scripts/post_panel.py 'panels/${PANEL_NAME}/${PANEL_NAME}.kicad_pro' \"\$PCB_FILE\" cd panels && kibot -e '${PANEL_NAME}/${PANEL_NAME}.kicad_sch' -c \"\$WORKDIR/.gitlab/configs/panel.kibot.yaml\" -d \"\$WORKDIR/Fabrication/panels\" " mv Fabrication/panels/*.zip Fabrication/ 2>/dev/null || true - uses: actions/upload-artifact@v3 with: name: Fabrication-panel path: Fabrication/ retention-days: 7 # ─── Astable upload ───────────────────────────────────────────────── # Mirrors astable_dev / astable_main on the GitLab side. The fab # artifacts above are downloaded into a shared Fabrication/ tree; # upload-bom.sh handles the POST + polling. astable_dev: if: github.ref == 'refs/heads/dev' || (github.event_name == 'pull_request' && github.base_ref == 'dev') runs-on: docker-host needs: [extract_version, generate_schematic, generate_bom, generate_3d, generate_gerbers, generate_position, generate_panel] env: VERSION: ${{ needs.extract_version.outputs.version }} ASTABLE_URL: ${{ secrets.ASTABLE_URL }} ASTABLE_API_TOKEN: ${{ secrets.ASTABLE_API_TOKEN }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - uses: actions/download-artifact@v3 with: { name: Fabrication, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-bom, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-3d, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-gerbers, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-position, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-panel, path: Fabrication } continue-on-error: true - name: Astable dev upload env: COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | # Wrapped in bash heredoc so commit-message metachars don't # leak. upload-bom.sh lives in the kicad-ci submodule. install -m 0755 .gitlab/scripts/upload-bom.sh /tmp/upload-bom.sh FAB="$PWD/Fabrication" SUB="$FAB/${PROJECT_NAME}" shopt -s nullglob # KiBot names outputs __.. The segment is # empty when the schematic has no ${rev} revision field, so match it # with a glob (`_*_`) rather than hard-coding the pipeline version. add() { local flag="$1"; shift; local m=( "$@" ); if [ ${#m[@]} -gt 0 ]; then ARGS+=("$flag" "${m[0]}"); fi; } ARGS=( --part-ipn "$PROJECT_NAME" --branch dev --commit-sha "${{ github.sha }}" --commit-message "$COMMIT_MESSAGE" ) add --file "$FAB"/${PROJECT_NAME}_*_bom.csv add --schematic-pdf "$FAB"/${PROJECT_NAME}_*_schematic.pdf add --step "$FAB"/${PROJECT_NAME}_*.step add --gerbers "$FAB"/${PROJECT_NAME}_*_JLC.zip add --panel-gerbers "$FAB"/${PROJECT_NAME}_panel_*_JLC.zip add --cpl "$FAB"/${PROJECT_NAME}_*_cpl.csv add --ibom-html "$SUB"/${PROJECT_NAME}_*_ibom.html add --bom-xlsx "$SUB"/${PROJECT_NAME}_*_bom.xlsx add --pcba-image "$SUB"/PCBA_${PROJECT_NAME}_*.png add --pcb-image "$SUB"/PCB_${PROJECT_NAME}_*.png /tmp/upload-bom.sh "${ARGS[@]}" astable_main: if: github.ref == 'refs/heads/main' runs-on: docker-host needs: [extract_version, generate_schematic, generate_bom, generate_3d, generate_gerbers, generate_position, generate_panel] env: VERSION: ${{ needs.extract_version.outputs.version }} ASTABLE_URL: ${{ secrets.ASTABLE_URL }} ASTABLE_API_TOKEN: ${{ secrets.ASTABLE_API_TOKEN }} steps: - uses: actions/checkout@v4 with: { submodules: recursive } - uses: actions/download-artifact@v3 with: { name: Fabrication, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-bom, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-3d, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-gerbers, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-position, path: Fabrication } - uses: actions/download-artifact@v3 with: { name: Fabrication-panel, path: Fabrication } continue-on-error: true - name: Astable main upload env: COMMIT_MESSAGE: ${{ github.event.head_commit.message }} run: | install -m 0755 .gitlab/scripts/upload-bom.sh /tmp/upload-bom.sh FAB="$PWD/Fabrication" SUB="$FAB/${PROJECT_NAME}" VAL="$PWD/Validation" shopt -s nullglob # See astable_dev: match KiBot's __ outputs by glob. add() { local flag="$1"; shift; local m=( "$@" ); if [ ${#m[@]} -gt 0 ]; then ARGS+=("$flag" "${m[0]}"); fi; } ARGS=( --part-ipn "$PROJECT_NAME" --branch main --commit-sha "${{ github.sha }}" --commit-message "$COMMIT_MESSAGE" ) add --file "$FAB"/${PROJECT_NAME}_*_bom.csv add --cpl "$FAB"/${PROJECT_NAME}_*_cpl.csv add --gerbers "$FAB"/${PROJECT_NAME}_*_JLC.zip add --panel-gerbers "$FAB"/${PROJECT_NAME}_panel_*_JLC.zip add --step "$FAB"/${PROJECT_NAME}_*.step add --schematic-pdf "$FAB"/${PROJECT_NAME}_*_schematic.pdf add --pcb-pdf "$SUB"/${PROJECT_NAME}_*_PCB.pdf add --ibom-html "$SUB"/${PROJECT_NAME}_*_ibom.html add --bom-xlsx "$SUB"/${PROJECT_NAME}_*_bom.xlsx add --pcba-image "$SUB"/PCBA_${PROJECT_NAME}_*.png add --pcb-image "$SUB"/PCB_${PROJECT_NAME}_*.png add --erc-report "$VAL"/${PROJECT_NAME}-erc.html add --drc-report "$VAL"/${PROJECT_NAME}-drc.html /tmp/upload-bom.sh "${ARGS[@]}" - uses: actions/upload-artifact@v3 if: always() with: name: Fabrication-release path: Fabrication/ retention-days: 7