diff --git a/.github/workflows/_image-factory.yml b/.github/workflows/_image-factory.yml new file mode 100644 index 0000000..78fcf59 --- /dev/null +++ b/.github/workflows/_image-factory.yml @@ -0,0 +1,500 @@ +name: _image-factory +on: + workflow_call: + inputs: + image-name: + required: true + type: string + image-tag: + type: string + default: '' + mapchete-ref: + type: string + default: '' + mapchete-eo-ref: + type: string + default: '' + mapchete-hub-ref: + type: string + default: '' + mapchete-hub-cli-ref: + type: string + default: '' + +jobs: + prepare: + runs-on: ubuntu-latest + outputs: + run-build: ${{ steps.check.outputs.run }} + mapchete-ref: ${{ steps.resolve.outputs.mapchete }} + mapchete-eo-ref: ${{ steps.resolve.outputs.eo }} + mapchete-hub-ref: ${{ steps.resolve.outputs.hub }} + mapchete-hub-cli-ref: ${{ steps.resolve.outputs.cli }} + calculated-tag: ${{ steps.version.outputs.tag }} + mode: ${{ steps.version.outputs.mode }} + use_branch: ${{ steps.version.outputs.use_branch }} + target_latest: ${{ steps.version.outputs.target_latest }} + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - id: check + run: | + if [[ "${{ github.event_name }}" == "repository_dispatch" ]]; then + S="${{ github.event.client_payload.source_repo }}" + T="${{ inputs.image-name }}" + if [[ "$T" == "mapchete" && "$S" =~ ^mapchete(-eo|-hub)?$ ]]; then echo "run=true" >> $GITHUB_OUTPUT + elif [[ "$T" =~ ^mhub-cli-(light|full)$ && "$S" == "mapchete-hub-cli" ]]; then echo "run=true" >> $GITHUB_OUTPUT + else echo "run=false" >> $GITHUB_OUTPUT; fi + else echo "run=true" >> $GITHUB_OUTPUT; fi + + - id: version + name: Detect Mode and Target Branch + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + EVENT="${{ github.event_name }}" + P_TYPE="${{ github.event.client_payload.type }}" + PREFIX="$(date +'%Y.%-m')." + git fetch origin + + # 1. RELEASE MODE (Tags) + if [[ "${{ github.ref_type }}" == "tag" ]] || [[ "$EVENT" == "release" ]]; then + MODE="release" + TAG_NAME="${{ github.ref_name }}" + [[ "$EVENT" == "release" ]] && TAG_NAME="${{ github.event.release.tag_name }}" + USE_BRANCH="$TAG_NAME" + TARGET_LATEST="true" + + # 2. RC or PR MODE (Version Computation Needed) + elif [[ "$EVENT" == "pull_request" ]] || [[ "$P_TYPE" == "version" ]] || [[ "$EVENT" == "workflow_dispatch" && -z "${{ inputs.image-tag }}" ]]; then + # Determine if this is a PR or an RC dispatch + if [[ "$EVENT" == "pull_request" ]]; then + MODE="pr" + TARGET_LATEST="false" + USE_BRANCH="${{ github.head_ref }}" + else + MODE="rc" + TARGET_LATEST="true" + USE_BRANCH="${{ github.ref_name }}" + fi + + # --- COMPUTE NEXT VERSION --- + # Search for an existing RC branch for this month + EXIST_RC=$(git branch -r | grep "origin/rc/${{ inputs.image-name }}-$PREFIX" | head -n 1 | sed 's|.*origin/||' || echo "") + + if [[ -n "$EXIST_RC" ]]; then + # Re-use version from existing RC branch + USE_BRANCH="$EXIST_RC" + TAG_NAME=$(echo "$EXIST_RC" | sed 's/.*-//') + else + # Query GHCR for the highest existing patch number for this month + TAGS=$(gh api /orgs/mapchete/packages/container/${{ inputs.image-name }}/versions --jq '.[].metadata.container.tags[]' 2>/dev/null || echo "") + LATEST_CTR=$(echo "$TAGS" | grep "^$PREFIX" | sed "s/$PREFIX//" | sort -nr | head -n1) + + if [[ -z "$LATEST_CTR" ]]; then + TAG_NAME="${PREFIX}0" + else + TAG_NAME="${PREFIX}$((LATEST_CTR + 1))" + fi + fi + # ---------------------------- + + # 3. DEV MODE (Manual builds with specific tags or branch pushes) + else + MODE="dev" + TAG_NAME="${{ inputs.image-tag || 'dev' }}" + USE_BRANCH="${{ github.ref_name }}" + TARGET_LATEST="false" + fi + + echo "tag=$TAG_NAME" >> $GITHUB_OUTPUT + echo "mode=$MODE" >> $GITHUB_OUTPUT + echo "use_branch=$USE_BRANCH" >> $GITHUB_OUTPUT + echo "target_latest=$TARGET_LATEST" >> $GITHUB_OUTPUT + + - name: Pivot to RC Branch + if: steps.version.outputs.mode == 'rc' && steps.version.outputs.use_branch != github.ref_name + run: git checkout ${{ steps.version.outputs.use_branch }} + + - id: resolve + name: Resolve Component References + run: | + touch versions.yml + MODE="${{ steps.version.outputs.mode }}" + # Track determines if we look at _RELEASE or _ROLLING in versions.yml + TRACK=$([[ "$MODE" == "rc" || "$MODE" == "release" ]] && echo "_RELEASE" || echo "_ROLLING") + + get_cur() { + local val=$(grep "^$1:" versions.yml | awk -F': ' '{print $2}' | tr -d '\r\n ' || echo "") + [[ -z "$val" || "$val" == "null" ]] && echo "main" || echo "$val" + } + + M_CUR=$(get_cur "MAPCHETE_REF${TRACK}") + EO_CUR=$(get_cur "MAPCHETE_EO_REF${TRACK}") + HUB_CUR=$(get_cur "MAPCHETE_HUB_REF${TRACK}") + CLI_CUR=$(get_cur "MAPCHETE_HUB_CLI_REF${TRACK}") + + # 1. DISPATCH LOGIC (Mode-Aware) + D_REPO="${{ github.event.client_payload.source_repo }}" + P_SHA="${{ github.event.client_payload.sha }}" + P_TAG="${{ github.event.client_payload.image_tag }}" + + # If we are in RC/Release mode, prioritize the Tag (2026.x.x) + # If in Dev/Rolling mode, prioritize the SHA + if [[ "$MODE" == "rc" || "$MODE" == "release" ]]; then + D_REF=$([[ -n "$P_TAG" && "$P_TAG" != "null" ]] && echo "$P_TAG" || echo "$P_SHA") + else + D_REF=$([[ -n "$P_SHA" && "$P_SHA" != "null" ]] && echo "$P_SHA" || echo "$P_TAG") + fi + + # 2. RESOLUTION FUNCTION + resolve_ref() { + local repo_name=$1 + local manual_input=$2 + local current_track_val=$3 + + # Priority 1: Triggering Repo (The Dispatcher) + if [[ "$D_REPO" == "$repo_name" && -n "$D_REF" && "$D_REF" != "null" ]]; then + echo "$D_REF" + # Priority 2: Manual Workflow Input + elif [[ -n "$manual_input" && "$manual_input" != "null" ]]; then + echo "$manual_input" + # Priority 3: versions.yml Fallback (Crucial for MAPCHETE_HUB_REF) + else + echo "$current_track_val" + fi + } + + # 3. Execution + M_F=$(resolve_ref "mapchete" "${{ inputs.mapchete-ref }}" "$M_CUR") + EO_F=$(resolve_ref "mapchete-eo" "${{ inputs.mapchete-eo-ref }}" "$EO_CUR") + H_F=$(resolve_ref "mapchete-hub" "${{ inputs.mapchete-hub-ref }}" "$HUB_CUR") + C_F=$(resolve_ref "mapchete-hub-cli" "${{ inputs.mapchete-hub-cli-ref }}" "$CLI_CUR") + + # Normalize function ensures we never pass empty strings to build-args + normalize() { + local v=$(echo "$1" | tr -d ' ') + [[ -z "$v" || "$v" == "null" || "$v" == "latest" ]] && echo "main" || echo "$v" + } + + echo "mapchete=$(normalize $M_F)" >> $GITHUB_OUTPUT + echo "eo=$(normalize $EO_F)" >> $GITHUB_OUTPUT + echo "hub=$(normalize $H_F)" >> $GITHUB_OUTPUT + echo "cli=$(normalize $C_F)" >> $GITHUB_OUTPUT + + - name: Generate Summary + if: always() + run: | + # 1. Decision Header + if [[ "${{ steps.check.outputs.run }}" == "true" ]]; then + DECISION="## โฉ BUILD PROCEEDING" + else + DECISION="## ๐Ÿ›‘ BUILD SKIPPED" + fi + + MODE="${{ steps.version.outputs.mode }}" + TAG="${{ steps.version.outputs.tag }}" + LATEST="${{ steps.version.outputs.target_latest }}" + EVENT="${{ github.event_name }}" + + # 2. Track Identification Logic + if [[ "$MODE" == "rc" || "$MODE" == "release" || "$EVENT" == "pull_request" ]]; then + ACTIVE_TRACK="RELEASE ๐ŸงŠ (Pinned)" + else + ACTIVE_TRACK="ROLLING ๐ŸŒ€ (Latest)" + fi + + # 3. Registry Tag Preview + if [[ "$MODE" == "rc" ]]; then + FINAL_TAGS="\`rc-$TAG\`" + else + FINAL_TAGS="\`$TAG\`" + fi + if [[ "$LATEST" == "true" || "${{ github.ref_name }}" == "main" ]]; then + FINAL_TAGS="$FINAL_TAGS, \`latest\`" + fi + + # 4. Write Main Table + echo "$DECISION" >> $GITHUB_STEP_SUMMARY + echo "---" >> $GITHUB_STEP_SUMMARY + echo "### ๐Ÿ› ๏ธ Build Preparation Summary" >> $GITHUB_STEP_SUMMARY + + echo "| Property | Value |" >> $GITHUB_STEP_SUMMARY + echo "| :--- | :--- |" >> $GITHUB_STEP_SUMMARY + echo "| **Target Image** | \`${{ inputs.image-name }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Operation Mode** | \`${MODE:-N/A}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Resolution Track** | **$ACTIVE_TRACK** |" >> $GITHUB_STEP_SUMMARY + echo "| **Registry Tags** | **$FINAL_TAGS** |" >> $GITHUB_STEP_SUMMARY + echo "| **Trigger Event** | \`${EVENT}\` |" >> $GITHUB_STEP_SUMMARY + + # 5. Repository Dispatch Details (Conditional) + if [[ "$EVENT" == "repository_dispatch" ]]; then + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### ๐Ÿ“ก Incoming Dispatch Payload" >> $GITHUB_STEP_SUMMARY + echo "| Field | Value |" >> $GITHUB_STEP_SUMMARY + echo "| :--- | :--- |" >> $GITHUB_STEP_SUMMARY + echo "| **Source Repo** | \`${{ github.event.client_payload.source_repo }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Event Type** | \`${{ github.event.client_payload.type }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **Payload Ref** | \`${{ github.event.client_payload.image_tag || github.event.client_payload.sha || 'N/A' }}\` |" >> $GITHUB_STEP_SUMMARY + fi + + # 6. Final Resolved Versions (The current 'Truth' for this build) + echo "" >> $GITHUB_STEP_SUMMARY + echo "#### ๐Ÿ“ฆ Resolved Component Versions" >> $GITHUB_STEP_SUMMARY + echo "> These versions are derived from the **$ACTIVE_TRACK** track, potentially overridden by manual inputs or dispatch payloads." >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "| Component | Resolved Reference |" >> $GITHUB_STEP_SUMMARY + echo "| :--- | :--- |" >> $GITHUB_STEP_SUMMARY + echo "| **mapchete** | \`${{ steps.resolve.outputs.mapchete || 'main' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **mapchete-eo** | \`${{ steps.resolve.outputs.eo || 'main' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **mapchete-hub** | \`${{ steps.resolve.outputs.hub || 'main' }}\` |" >> $GITHUB_STEP_SUMMARY + echo "| **mapchete-hub-cli** | \`${{ steps.resolve.outputs.cli || 'main' }}\` |" >> $GITHUB_STEP_SUMMARY + + build: + needs: prepare + if: needs.prepare.outputs.run-build == 'true' + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + pull-requests: write + steps: + - uses: actions/checkout@v4 + with: + ref: ${{ needs.prepare.outputs.use_branch }} + + - name: Build Local Image + uses: docker/build-push-action@v5 + with: + context: ${{ inputs.image-name }}/ + push: false + load: true + tags: local-test-image:latest + build-args: | + MAPCHETE_REF=${{ needs.prepare.outputs.mapchete-ref }} + MAPCHETE_EO_REF=${{ needs.prepare.outputs.mapchete-eo-ref }} + MAPCHETE_HUB_REF=${{ needs.prepare.outputs.mapchete-hub-ref }} + MAPCHETE_HUB_CLI_REF=${{ needs.prepare.outputs.mapchete-hub-cli-ref }} + + - name: Image Fingerprint Check + id: image-fingerprint-check + run: | + LOCAL_FP=$(docker run --rm --entrypoint /bin/sh local-test-image:latest -c "cat /app/uv_fingerprint" | tr -d '\r\n ') + echo "fingerprint=$LOCAL_FP" >> $GITHUB_OUTPUT + MODE="${{ needs.prepare.outputs.mode }}" + VER="${{ needs.prepare.outputs.calculated-tag }}" + if [[ "$MODE" == "rc" ]]; then REMOTE_TAG="rc-$VER" + elif [[ "$MODE" == "release" ]]; then REMOTE_TAG="$VER" + elif [[ "${{ github.ref_name }}" == "main" ]]; then REMOTE_TAG="latest" + else REMOTE_TAG="dev"; fi + REMOTE_IMAGE="ghcr.io/mapchete/${{ inputs.image-name }}:$REMOTE_TAG" + + INSPECT_JSON=$(skopeo inspect docker://$REMOTE_IMAGE 2>/dev/null || echo '{"Labels":{}}') + REMOTE_FP=$(echo "$INSPECT_JSON" | jq -r '.Labels["org.mapchete.image_fingerprint"] // "none"' | tr -d '\r\n ') + + if [[ "$LOCAL_FP" == "$REMOTE_FP" ]]; then + echo "โœ… Fingerprint Match. Image is up to date." + echo "changed=false" >> $GITHUB_OUTPUT + if [[ "$MODE" == "rc" ]]; then + docker pull $REMOTE_IMAGE + get_ver() { docker run --rm $REMOTE_IMAGE uv pip list | grep "^$1 " | awk '{print $2}' | tr -d '\r\n' || echo "N/A"; } + echo "m_ref=$(get_ver mapchete)" >> $GITHUB_OUTPUT + echo "eo_ref=$(get_ver mapchete-eo)" >> $GITHUB_OUTPUT + echo "hub_ref=$(get_ver mapchete-hub)" >> $GITHUB_OUTPUT + echo "cli_ref=$(get_ver mapchete-hub-cli)" >> $GITHUB_OUTPUT + fi + else + echo "โš ๏ธ Fingerprint Mismatch. Build required." + echo "changed=true" >> $GITHUB_OUTPUT + fi + + - name: Integration Tests + if: inputs.image-name == 'mapchete' && steps.image-fingerprint-check.outputs.changed == 'true' + run: | + docker run --rm \ + -e AWS_ACCESS_KEY_ID=${{ secrets.AWS_ACCESS_KEY_ID }} \ + -e AWS_SECRET_ACCESS_KEY=${{ secrets.AWS_SECRET_ACCESS_KEY }} \ + -e CDSE_S3_ACCESS_KEY=${{ secrets.CDSE_S3_ACCESS_KEY }} \ + -e CDSE_S3_SECRET_KEY=${{ secrets.CDSE_S3_SECRET_KEY }} \ + -e AWS_CA_BUNDLE=/etc/ssl/certs/ca-certificates.crt \ + local-test-image:latest \ + /bin/bash -c "cd deps/mapchete-eo && uv pip install -e .[test] && uv run pytest" + + - uses: docker/login-action@v3 + if: steps.image-fingerprint-check.outputs.changed == 'true' + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - id: meta + if: steps.image-fingerprint-check.outputs.changed == 'true' || needs.prepare.outputs.mode == 'rc' + uses: docker/metadata-action@v5 + with: + images: ghcr.io/mapchete/${{ inputs.image-name }} + flavor: | + latest=false + tags: | + type=raw,value=rc-${{ needs.prepare.outputs.calculated-tag }},enable=${{ needs.prepare.outputs.mode == 'rc' }} + type=raw,value=${{ needs.prepare.outputs.calculated-tag }},enable=${{ needs.prepare.outputs.mode != 'rc' && needs.prepare.outputs.mode != 'dev' }} + + # 3. Dev Tag + type=raw,value=dev,enable=${{ needs.prepare.outputs.mode == 'dev' }} + + # 4. Latest Logic + type=raw,value=latest,enable=${{ needs.prepare.outputs.target_latest == 'true' || github.ref == 'refs/heads/main' }} + + # 5. Manual Override Tag + type=raw,value=${{ inputs.image-tag }},enable=${{ inputs.image-tag != '' }} + + - name: Push Image + if: steps.image-fingerprint-check.outputs.changed == 'true' + uses: docker/build-push-action@v5 + with: + context: ${{ inputs.image-name }}/ + push: true + # We use the tags generated by the meta step above + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + build-args: | + MAPCHETE_REF=${{ needs.prepare.outputs.mapchete-ref }} + MAPCHETE_EO_REF=${{ needs.prepare.outputs.mapchete-eo-ref }} + MAPCHETE_HUB_REF=${{ needs.prepare.outputs.mapchete-hub-ref }} + MAPCHETE_HUB_CLI_REF=${{ needs.prepare.outputs.mapchete-hub-cli-ref }} + IMAGE_FINGERPRINT=${{ steps.image-fingerprint-check.outputs.fingerprint }} + + - name: Update Tracking + if: needs.prepare.outputs.mode == 'rc' + run: | + # 1. Setup Variables + PUSH_DATE=$(date +'%Y-%m-%d'); echo "PUSH_DATE=$PUSH_DATE" >> $GITHUB_ENV + FINAL_VER="${{ needs.prepare.outputs.calculated-tag }}" + + # 2. Select Release Track Pins (The Truth) + if [[ "${{ steps.image-fingerprint-check.outputs.changed }}" == "true" ]]; then + M_REF="${{ needs.prepare.outputs.mapchete-ref }}" + EO_REF="${{ needs.prepare.outputs.mapchete-eo-ref }}" + H_REF="${{ needs.prepare.outputs.mapchete-hub-ref }}" + C_REF="${{ needs.prepare.outputs.mapchete-hub-cli-ref }}" + else + M_REF="${{ steps.image-fingerprint-check.outputs.m_ref }}" + EO_REF="${{ steps.image-fingerprint-check.outputs.eo_ref }}" + H_REF="${{ steps.image-fingerprint-check.outputs.hub_ref }}" + C_REF="${{ steps.image-fingerprint-check.outputs.cli_ref }}" + fi + + # --- NEW: Update versions.yml --- + update_yaml() { + local key=$1 + local val=$2 + if [[ -n "$val" && "$val" != "null" && "$val" != "N/A" ]]; then + # Use sed to update the value for the key + sed -i "s/^${key}:.*/${key}: ${val}/" versions.yml + fi + } + + update_yaml "MAPCHETE_REF_RELEASE" "$M_REF" + update_yaml "MAPCHETE_EO_REF_RELEASE" "$EO_REF" + update_yaml "MAPCHETE_HUB_REF_RELEASE" "$H_REF" + update_yaml "MAPCHETE_HUB_CLI_REF_RELEASE" "$C_REF" + # ------------------------------- + + # 3. Handle CHANGELOG.rst (Your existing logic) + is_valid() { [[ -n "$1" && "$1" != "null" && "$1" != "N/A" ]]; } + TITLE="${FINAL_VER} - ${PUSH_DATE}" + LEN=${#TITLE} + UNDERLINE=$(printf '%0.s-' $(seq 1 $LEN)) + + if grep -q "^${FINAL_VER}" CHANGELOG.rst; then + START_LN=$(grep -n "^${FINAL_VER}" CHANGELOG.rst | head -n 1 | cut -d: -f1) + END_LN=$(tail -n +$((START_LN + 1)) CHANGELOG.rst | grep -n -m 1 "^[0-9]" | cut -d: -f1) + [ -z "$END_LN" ] && END_LN=$(wc -l < CHANGELOG.rst) || END_LN=$((START_LN + END_LN - 1)) + + sed -n "${START_LN},${END_LN}p" CHANGELOG.rst | grep -v "\* Aligned" > block.tmp + { + echo "* Aligned **mapchete** to \`$M_REF\`" + echo "* Aligned **mapchete-eo** to \`$EO_REF\`" + echo "* Aligned **mapchete-hub** to \`$H_REF\`" + is_valid "$C_REF" && echo "* Aligned **mapchete-hub-cli** to \`$C_REF\`" + } > bullets.tmp + sed -i "4r bullets.tmp" block.tmp + + head -n $((START_LN - 1)) CHANGELOG.rst > changelog.new + cat block.tmp >> changelog.new + tail -n +$((END_LN + 1)) CHANGELOG.rst >> changelog.new + mv changelog.new CHANGELOG.rst + else + { + echo "$TITLE" + echo "$UNDERLINE" + echo "" + echo "* Aligned **mapchete** to \`$M_REF\`" + echo "* Aligned **mapchete-eo** to \`$EO_REF\`" + echo "* Aligned **mapchete-hub** to \`$H_REF\`" + is_valid "$C_REF" && echo "* Aligned **mapchete-hub-cli** to \`$C_REF\`" + echo "" + } > new_block.tmp + head -n 4 CHANGELOG.rst > changelog.new + echo "" >> changelog.new; echo "" >> changelog.new + cat new_block.tmp >> changelog.new + tail -n +5 CHANGELOG.rst >> changelog.new + mv changelog.new CHANGELOG.rst + fi + + # Final Polish + cat -s CHANGELOG.rst | sed 's/[[:space:]]*$//' > CHANGELOG.tmp && mv CHANGELOG.tmp CHANGELOG.rst + + - name: Automated Version PR + if: needs.prepare.outputs.mode == 'rc' + uses: peter-evans/create-pull-request@v6 + with: + token: ${{ secrets.MAPCHETE_PAT || secrets.GITHUB_TOKEN }} + commit-message: "rc: sync release pins" + branch: "rc/${{ inputs.image-name }}-${{ needs.prepare.outputs.calculated-tag }}" + add-paths: | + versions.yml + CHANGELOG.rst + title: "๐Ÿงช RC: ${{ inputs.image-name }} ${{ needs.prepare.outputs.calculated-tag }}" + body: | + ### ๐Ÿงช Release Candidate Update + This PR freezes the component alignment for version `${{ needs.prepare.outputs.calculated-tag }}`. + + **Build Status:** ${{ steps.image-fingerprint-check.outputs.changed == 'true' && '๐Ÿš€ New Image Pushed' || 'โœ… Reusing Existing Image' }} + **Push Date:** `${{ env.PUSH_DATE }}` + + #### ๐Ÿ“ฆ Component Alignment (Release Track) + | Component | Reference | + | :--- | :--- | + ${{ (needs.prepare.outputs.mapchete-ref != '' && needs.prepare.outputs.mapchete-ref != 'N/A') && format('| **mapchete** | `{0}` |', needs.prepare.outputs.mapchete-ref || steps.image-fingerprint-check.outputs.m_ref) || '' }} + ${{ (needs.prepare.outputs.mapchete-eo-ref != '' && needs.prepare.outputs.mapchete-eo-ref != 'N/A') && format('| **mapchete-eo** | `{0}` |', needs.prepare.outputs.mapchete-eo-ref || steps.image-fingerprint-check.outputs.eo_ref) || '' }} + ${{ (needs.prepare.outputs.mapchete-hub-ref != '' && needs.prepare.outputs.mapchete-hub-ref != 'N/A') && format('| **mapchete-hub** | `{0}` |', needs.prepare.outputs.mapchete-hub-ref || steps.image-fingerprint-check.outputs.hub_ref) || '' }} + ${{ (needs.prepare.outputs.mapchete-hub-cli-ref != '' && needs.prepare.outputs.mapchete-hub-cli-ref != 'N/A') && format('| **mapchete-hub-cli** | `{0}` |', needs.prepare.outputs.mapchete-hub-cli-ref || steps.image-fingerprint-check.outputs.cli_ref) || '' }} + + --- + #### ๐Ÿ“ก Trigger Context + ${{ github.event_name == 'repository_dispatch' && format('**Source:** `{0}` (via Dispatch)', github.event.client_payload.source_repo) || '' }} + ${{ github.event_name == 'workflow_dispatch' && '**Source:** Manual Workflow Run' || '' }} + ${{ github.event_name == 'repository_dispatch' && format('**Payload Type:** `{0}`', github.event.client_payload.type) || '' }} + + --- + > **Note:** The table above only lists components currently present in this specific image build. + base: main + delete-branch: true + assignees: "scartography" + reviewers: "ungarj" + + cleanup-registry: + needs: [prepare, build] + if: always() && needs.prepare.outputs.run-build == 'true' + runs-on: ubuntu-latest + permissions: + packages: write + steps: + - name: Cleanup + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + TARGETS=$(gh api --paginate "/orgs/mapchete/packages/container/${{ inputs.image-name }}/versions" --jq '.[] | select(.metadata.container.tags | length == 0) | .id') + for ID in $TARGETS; do gh api --method DELETE "/orgs/mapchete/packages/container/${{ inputs.image-name }}/versions/$ID" || true; done \ No newline at end of file diff --git a/.github/workflows/build-controller.yml b/.github/workflows/build-controller.yml new file mode 100644 index 0000000..18b51bc --- /dev/null +++ b/.github/workflows/build-controller.yml @@ -0,0 +1,68 @@ +name: build-controller + +on: + push: + branches: [main] + tags: + - '[0-9]*.[0-9]*.[0-9]*' + release: + types: [published] + pull_request: + branches: [main] + repository_dispatch: + types: [source_update] + workflow_dispatch: + inputs: + image-tag: + description: 'Manual Tag override' + default: 'manual-build' + mapchete-ref: + description: 'mapchete branch/tag' + default: 'main' + mapchete-eo-ref: + description: 'mapchete-eo branch/tag' + default: 'main' + mapchete-hub-ref: + description: 'mapchete-hub branch/tag' + default: 'main' + mapchete-hub-cli-ref: + description: 'mapchete-hub-cli branch/tag' + default: 'main' + +concurrency: + group: ${{ github.workflow }}-${{ github.event.client_payload.source_repo || github.ref }} + cancel-in-progress: false + +jobs: + mapchete: + uses: ./.github/workflows/_image-factory.yml + with: + image-name: mapchete + image-tag: ${{ inputs.image-tag }} + mapchete-ref: ${{ inputs.mapchete-ref }} + mapchete-eo-ref: ${{ inputs.mapchete-eo-ref }} + mapchete-hub-ref: ${{ inputs.mapchete-hub-ref }} + mapchete-hub-cli-ref: ${{ inputs.mapchete-hub-cli-ref }} + secrets: inherit + + mhub-cli-light: + uses: ./.github/workflows/_image-factory.yml + with: + image-name: mhub-cli-light + image-tag: ${{ inputs.image-tag }} + mapchete-ref: ${{ inputs.mapchete-ref }} + mapchete-eo-ref: ${{ inputs.mapchete-eo-ref }} + mapchete-hub-ref: ${{ inputs.mapchete-hub-ref }} + mapchete-hub-cli-ref: ${{ inputs.mapchete-hub-cli-ref }} + secrets: inherit + + mhub-cli-full: + uses: ./.github/workflows/_image-factory.yml + with: + image-name: mhub-cli-full + image-tag: ${{ inputs.image-tag }} + mapchete-ref: ${{ inputs.mapchete-ref }} + mapchete-eo-ref: ${{ inputs.mapchete-eo-ref }} + mapchete-hub-ref: ${{ inputs.mapchete-hub-ref }} + mapchete-hub-cli-ref: ${{ inputs.mapchete-hub-cli-ref }} + secrets: inherit \ No newline at end of file diff --git a/.github/workflows/build-image.yml b/.github/workflows/build-image.yml deleted file mode 100644 index b3b72cd..0000000 --- a/.github/workflows/build-image.yml +++ /dev/null @@ -1,65 +0,0 @@ -name: docker build - -on: - push: - branches: - - 'main' # Run only on pushes to the main branch - tags: - - '*.*.*' # Run on version tags like 1.2.3 - pull_request: - branches: - - 'main' - -env: - REGISTRY: ghcr.io - -jobs: - build-and-push-image: - runs-on: ubuntu-latest - permissions: - contents: read - packages: write - attestations: write - id-token: write - - strategy: - matrix: - image-name: [mapchete, mhub-cli-light, mhub-cli-full] - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Log in to the Container registry - uses: docker/login-action@v3 - with: - registry: ${{ env.REGISTRY }} - username: ${{ github.actor }} - password: ${{ secrets.GITHUB_TOKEN }} - - - name: Extract metadata (tags, labels) for Docker - id: meta - uses: docker/metadata-action@v5 - with: - images: ${{ env.REGISTRY }}/mapchete/${{ matrix.image-name }} - tags: | - type=semver,pattern={{version}} - type=raw,value=latest,enable={{is_default_branch}} - type=ref,event=branch - type=ref,event=pr - type=ref,event=tag - - - name: Build and push ${{ matrix.image-name }} image - id: push - uses: docker/build-push-action@v5 - with: - context: ${{ matrix.image-name }}/ - push: true - tags: ${{ steps.meta.outputs.tags }} - labels: ${{ steps.meta.outputs.labels }} - - name: Generate artifact attestation - uses: actions/attest-build-provenance@v2 - with: - subject-name: ${{ env.REGISTRY }}/mapchete/${{ matrix.image-name}} - subject-digest: ${{ steps.push.outputs.digest }} - push-to-registry: true diff --git a/.github/workflows/registry-cleanup.yml b/.github/workflows/registry-cleanup.yml new file mode 100644 index 0000000..299778c --- /dev/null +++ b/.github/workflows/registry-cleanup.yml @@ -0,0 +1,38 @@ +name: registry cleanup +on: + workflow_run: + workflows: ["build controller"] + types: [completed] + schedule: + - cron: '0 0 * * 0' + workflow_dispatch: +jobs: + cleanup: + runs-on: ubuntu-latest + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + permissions: + packages: write + strategy: + matrix: + image-name: [mapchete, mhub-cli-light, mhub-cli-full] + steps: + - name: Purge Untagged and Attestation Tags + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + ORG="mapchete" + PKG="${{ matrix.image-name }}" + echo "Cleaning $ORG/$PKG..." + TARGET_VERSIONS=$(gh api --paginate "/orgs/$ORG/packages/container/$PKG/versions" --jq ' + .[] | select( + (.metadata.container.tags | length == 0) or + (.metadata.container.tags[] | startswith("sha256-")) + ) | .id') + if [ -z "$TARGET_VERSIONS" ]; then + echo "Registry already clean." + else + for ID in $TARGET_VERSIONS; do + echo "Deleting version: $ID" + gh api --method DELETE "/orgs/$ORG/packages/container/$PKG/versions/$ID" || true + done + fi \ No newline at end of file diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 3104932..822bb54 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -2,8 +2,14 @@ Changelog ######### - +2026.2.0 - 2026-02-05 --------------------- + +* Aligned **mapchete** to `2026.2.1` +* Aligned **mapchete-eo** to `2026.2.0` +* Aligned **mapchete-hub** to `2026.2.1` +* Aligned **mapchete-hub-cli** to `2025.9.0` + 2026.1.1 - 2026-01-12 --------------------- @@ -13,7 +19,6 @@ Changelog * `aiobotocore` to `>=1.1.2,<3.0.0` - --------------------- 2026.1.0 - 2026-01-09 --------------------- @@ -24,7 +29,6 @@ Changelog * `mapchete-eo` to `2026.1.0` - ---------------------- 2025.11.0 - 2025-11-20 ---------------------- @@ -42,7 +46,6 @@ Changelog * `distributed` to `2025.11.0` * `fastapi` to `0.121.3` - ---------------------- 2025.10.0 - 2025-10-14 ---------------------- @@ -52,7 +55,6 @@ Changelog * `mapchete`: updated `mapchete` to `2025.10.1` * `mapchete`: updated `mapchete-eo` to `2025.10.1` - --------------------- 2025.9.2 - 2025-09-26 --------------------- @@ -65,7 +67,6 @@ Changelog * fixed image build on tagged releases - --------------------- 2025.9.1 - 2025-09-02 --------------------- @@ -74,7 +75,6 @@ Changelog * added `mhub-cli-light` and `mhub-cli-full` images (#3) - --------------------- 2025.9.0 - 2025-09-02 --------------------- @@ -91,7 +91,6 @@ Changelog * ``mapchete-eo`` to ``2025.8.3`` - --------------------- 2025.8.1 - 2025-08-14 --------------------- @@ -105,7 +104,6 @@ Changelog * keep ``uv`` installed for further use * make ``/app`` the working directory - --------------------- 2025.8.0 - 2025-08-12 --------------------- diff --git a/README.rst b/README.rst index 46de28f..6b1fc92 100644 --- a/README.rst +++ b/README.rst @@ -6,8 +6,8 @@ Mapchete Container Images :target: https://github.com/mapchete/container-images/releases :alt: Latest Release -.. image:: https://img.shields.io/github/actions/workflow/status/mapchete/container-images/build-image.yml - :target: https://github.com/mapchete/container-images/actions/workflows/build-image.yml +.. image:: https://img.shields.io/github/actions/workflow/status/mapchete/container-images/build-controller.yml + :target: https://github.com/mapchete/container-images/actions/workflows/build-controller.yml :alt: Build Status .. image:: https://img.shields.io/github/license/mapchete/container-images @@ -23,7 +23,7 @@ The image is hosted on the GitHub Container Registry (ghcr.io). mapchete ~~~~~~~~ -The image is based on ``ghcr.io/osgeo/gdal:ubuntu-small-3.11.3`` (using Python ``3.12.3`` and GDAL ``3.11.3``) with the most recent versions of mapchete including the mapchete EO extension and mapchete Hub. +The image is based on ``ghcr.io/osgeo/gdal:ubuntu-small-3.12.0`` (using Python ``3.12.3`` and GDAL ``3.12.0``) with the most recent versions of mapchete including the mapchete EO extension and mapchete Hub. .. code-block:: shell @@ -36,7 +36,36 @@ Automated Cleanup This repository uses an automated workflow to clean up old, untagged images. The following tags are always kept: * ``latest`` +* ``dev`` * Version tags (e.g., ``2025.8.1``) All other images (i.e., those tagged with a commit SHA) are subject to cleanup to keep the registry tidy. + +Build Control +------------- + +Images can be manually rebuilt via the `build-controller `_ workflow. This allows for manual triggers with custom tags and repository references. + + +Version Management +------------------ + +The default versions (branches, tags, or SHAs) for the dependent repositories are managed in the `versions.yml `_ file. + +.. code-block:: yaml + + MAPCHETE_REF_RELEASE: 2026.2.1 + MAPCHETE_EO_REF_RELEASE: 2026.2.0 + MAPCHETE_HUB_REF_RELEASE: 2025.11.0 + MAPCHETE_HUB_CLI_REF_RELEASE: 2025.9.0 + + + MAPCHETE_REF_ROLLING: main + MAPCHETE_EO_REF_ROLLING: main + MAPCHETE_HUB_REF_ROLLING: main + MAPCHETE_HUB_CLI_REF_ROLLING: main + + +When creating a release tag in this repository, these versions are "frozen" for that specific image tag, ensuring reproducible builds. + diff --git a/mapchete/Dockerfile b/mapchete/Dockerfile index d907a2a..78af07c 100644 --- a/mapchete/Dockerfile +++ b/mapchete/Dockerfile @@ -1,43 +1,83 @@ FROM ghcr.io/osgeo/gdal:ubuntu-small-3.12.0 AS builder -ENV WORKDIR="/app" -ENV GML_SKIP_CORRUPTED_FEATURES=YES -ENV UV_COMPILE_BYTECODE=1 -ENV UV_LINK_MODE=copy -ENV PYTHONUNBUFFERED=1 -ENV PYTHONDONTWRITEBYTECODE=1 -ENV PATH="${WORKDIR}/.venv/bin:${PATH}" +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + curl \ + ca-certificates \ + && rm -rf /var/lib/apt/lists/* -WORKDIR ${WORKDIR} - -# install uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -# install system requirements and create virtual environment -RUN apt-get update \ - && apt install -y --no-install-recommends \ - git \ - && rm -rf /var/lib/apt/lists/* \ - && uv venv --no-managed-python ${WORKDIR}/.venv +ARG MAPCHETE_REF=main +ARG MAPCHETE_EO_REF=main +ARG MAPCHETE_HUB_REF=main + +WORKDIR /app +RUN mkdir -p deps/mapchete deps/mapchete-eo deps/mapchete-hub + +RUN git clone https://github.com/mapchete/mapchete deps/mapchete && \ + cd deps/mapchete && git checkout ${MAPCHETE_REF} + +RUN git clone https://github.com/mapchete/mapchete-eo deps/mapchete-eo && \ + cd deps/mapchete-eo && git checkout ${MAPCHETE_EO_REF} + +RUN git clone https://github.com/mapchete/mapchete-hub deps/mapchete-hub && \ + cd deps/mapchete-hub && git checkout ${MAPCHETE_HUB_REF} + +RUN uv export --frozen --project deps/mapchete >> raw.txt && \ + uv export --frozen --project deps/mapchete-eo >> raw.txt && \ + uv export --frozen --project deps/mapchete-hub >> raw.txt && \ + grep '==' raw.txt | \ + grep -v "mapchete" | \ + sed 's/[[:space:];\\].*//' | sort -V | \ + awk -F'==' '{a[$1]=$2} END {for (i in a) print i"=="a[i]}' | sort > requirements.txt + +RUN uv init --name factory-build --lib . -# install python requirements into virtual environment -COPY requirements.in . -RUN uv pip compile requirements.in -o requirements.txt \ - && uv pip sync requirements.txt \ - && rm requirements.* +RUN MAPCHETE_SHA=$(git -C deps/mapchete rev-parse HEAD) && \ + EO_SHA=$(git -C deps/mapchete-eo rev-parse HEAD) && \ + HUB_SHA=$(git -C deps/mapchete-hub rev-parse HEAD) && \ + uv add --constraints requirements.txt \ + "mapchete[complete] @ git+https://github.com/mapchete/mapchete.git@$MAPCHETE_SHA" \ + "mapchete-eo @ git+https://github.com/mapchete/mapchete-eo.git@$EO_SHA" \ + "mapchete-hub[deployment] @ git+https://github.com/mapchete/mapchete-hub.git@$HUB_SHA" -FROM ghcr.io/osgeo/gdal:ubuntu-small-3.12.0 AS runner +# Generate the fingerprint based on the resolved lockfile +# Combine the lockfile hash AND a hash of the source code +# to ensure code-only changes are captured +RUN sha256sum uv.lock > /app/combined_hash && \ + # 1. Find all relevant files (py, toml, yaml) + # 2. Exclude .git and hidden directories + # 3. Use 'cat' to hash content only (ignoring mtime) + # 4. Sort to ensure identical order + find deps/ -type f \( -name "*.py" -o -name "*.toml" -o -name "*.yaml" \) \ + -not -path '*/.*' | sort | xargs sha256sum >> /app/combined_hash && \ + # Create final fingerprint + sha256sum /app/combined_hash | cut -d' ' -f1 > /app/uv_fingerprint -ENV WORKDIR="/app" -ENV UV_COMPILE_BYTECODE=1 -ENV UV_LINK_MODE=copy -ENV UV_NO_CACHE=YES -ENV PATH="${WORKDIR}/.venv/bin:${PATH}" -WORKDIR ${WORKDIR} +FROM ghcr.io/osgeo/gdal:ubuntu-small-3.12.0 + +ARG IMAGE_FINGERPRINT=unknown + +RUN apt-get update && apt-get install -y --no-install-recommends \ + libglib2.0-0 \ + libxcb1 \ + && rm -rf /var/lib/apt/lists/* -# make sure uv is available further on COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -# copy venv from builder -COPY --from=builder "${WORKDIR}/.venv" "${WORKDIR}/.venv" +WORKDIR /app + +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/deps /app/deps +COPY --from=builder /app/uv_fingerprint /app/uv_fingerprint + +COPY . . + +LABEL org.mapchete.image_fingerprint=$IMAGE_FINGERPRINT +LABEL org.mapchete.uv_lock_hash=$IMAGE_FINGERPRINT + +ENV PATH="/app/.venv/bin:$PATH" + +CMD ["mapchete", "--help"] \ No newline at end of file diff --git a/mapchete/requirements.in b/mapchete/requirements.in deleted file mode 100644 index 55af953..0000000 --- a/mapchete/requirements.in +++ /dev/null @@ -1,22 +0,0 @@ -aiobotocore>=1.1.2,<3.0.0 -aiohttp -bokeh -dask==2025.11.0 -dask-gateway==2025.4.0 -dask-gateway-server==2025.4.0 -dask-kubernetes==2025.7.0 -distributed==2025.11.0 -fastapi==0.121.3 -httpx -jupyter-server-proxy -kubernetes -lz4 -mapchete @ git+https://github.com/mapchete/mapchete.git@2025.11.0 -mapchete-eo @ git+https://github.com/mapchete/mapchete-eo.git@2026.1.0 -mapchete-hub @ git+https://github.com/mapchete/mapchete-hub.git@2025.11.0 -prometheus-client -pydantic>=2.0.0 -pymongo -slack_sdk -sqlalchemy -uvicorn diff --git a/mhub-cli-full/Dockerfile b/mhub-cli-full/Dockerfile index cb07bff..3dd428c 100644 --- a/mhub-cli-full/Dockerfile +++ b/mhub-cli-full/Dockerfile @@ -1,33 +1,57 @@ -FROM ghcr.io/snakepacker/python/3.13 AS builder +FROM ghcr.io/osgeo/gdal:ubuntu-small-3.12.0 AS builder -ENV WORKDIR="/home" -ENV PATH="${WORKDIR}/.venv/bin:${PATH}" -WORKDIR ${WORKDIR} +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl ca-certificates build-essential python3-dev && rm -rf /var/lib/apt/lists/* -# install uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -# install system requirements and create virtual environment -RUN apt update \ - && apt install -y --no-install-recommends \ - git \ - && rm -rf /var/lib/apt/lists/* -RUN uv venv --no-managed-python ${WORKDIR}/.venv +ARG MAPCHETE_HUB_CLI_REF=main -# install python requirements into virtual environment -COPY requirements.in . -RUN uv pip compile requirements.in -o requirements.txt \ - && uv pip sync requirements.txt \ - && rm requirements.* +WORKDIR /app +RUN mkdir -p deps/mapchete-hub-cli +RUN git clone https://github.com/mapchete/mapchete-hub-cli deps/mapchete-hub-cli && \ + cd deps/mapchete-hub-cli && git checkout ${MAPCHETE_HUB_CLI_REF} -FROM ghcr.io/snakepacker/python/3.13 AS runner +RUN uv export --frozen --project deps/mapchete-hub-cli >> raw.txt && \ + grep '==' raw.txt | \ + grep -v "mapchete" | \ + sed 's/[[:space:];\\].*//' | sort -V | \ + awk -F'==' '{a[$1]=$2} END {for (i in a) print i"=="a[i]}' | sort > requirements.txt -ENV WORKDIR="/home" -ENV PATH="${WORKDIR}/.venv/bin:${PATH}" -WORKDIR ${WORKDIR} +RUN uv init --name factory-build --lib . && \ + uv add -c requirements.txt "./deps/mapchete-hub-cli[zones]" -# copy venv from builder -COPY --from=builder "${WORKDIR}/.venv" "${WORKDIR}/.venv" +# Generate the fingerprint based on the resolved lockfile +# Combine the lockfile hash AND a hash of the source code +# to ensure code-only changes are captured +RUN sha256sum uv.lock > /app/combined_hash && \ + # 1. Find all relevant files (py, toml, yaml) + # 2. Exclude .git and hidden directories + # 3. Use 'cat' to hash content only (ignoring mtime) + # 4. Sort to ensure identical order + find deps/ -type f \( -name "*.py" -o -name "*.toml" -o -name "*.yaml" \) \ + -not -path '*/.*' | sort | xargs sha256sum >> /app/combined_hash && \ + # Create final fingerprint + sha256sum /app/combined_hash | cut -d' ' -f1 > /app/uv_fingerprint -ENTRYPOINT [ "mhub" ] + +FROM ghcr.io/osgeo/gdal:ubuntu-small-3.12.0 + +ARG IMAGE_FINGERPRINT=unknown + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /app + +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/deps /app/deps +COPY --from=builder /app/uv_fingerprint /app/uv_fingerprint + +COPY . . + +LABEL org.mapchete.image_fingerprint=$IMAGE_FINGERPRINT +LABEL org.mapchete.uv_lock_hash=$IMAGE_FINGERPRINT + +ENV PATH="/app/.venv/bin:$PATH" +ENTRYPOINT ["mhub"] \ No newline at end of file diff --git a/mhub-cli-full/requirements.in b/mhub-cli-full/requirements.in deleted file mode 100644 index 14de97a..0000000 --- a/mhub-cli-full/requirements.in +++ /dev/null @@ -1 +0,0 @@ -mapchete-hub-cli[zones] @ git+https://github.com/mapchete/mapchete-hub-cli.git@2025.9.0 \ No newline at end of file diff --git a/mhub-cli-light/Dockerfile b/mhub-cli-light/Dockerfile index cb07bff..451e89e 100644 --- a/mhub-cli-light/Dockerfile +++ b/mhub-cli-light/Dockerfile @@ -1,33 +1,58 @@ -FROM ghcr.io/snakepacker/python/3.13 AS builder +FROM ghcr.io/osgeo/gdal:ubuntu-small-3.12.0 AS builder -ENV WORKDIR="/home" -ENV PATH="${WORKDIR}/.venv/bin:${PATH}" -WORKDIR ${WORKDIR} +RUN apt-get update && apt-get install -y --no-install-recommends \ + git curl ca-certificates && rm -rf /var/lib/apt/lists/* -# install uv COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ -# install system requirements and create virtual environment -RUN apt update \ - && apt install -y --no-install-recommends \ - git \ - && rm -rf /var/lib/apt/lists/* -RUN uv venv --no-managed-python ${WORKDIR}/.venv +ARG MAPCHETE_HUB_CLI_REF=main -# install python requirements into virtual environment -COPY requirements.in . -RUN uv pip compile requirements.in -o requirements.txt \ - && uv pip sync requirements.txt \ - && rm requirements.* +WORKDIR /app +RUN mkdir -p deps/mapchete-hub-cli +RUN git clone https://github.com/mapchete/mapchete-hub-cli deps/mapchete-hub-cli && \ + cd deps/mapchete-hub-cli && git checkout ${MAPCHETE_HUB_CLI_REF} -FROM ghcr.io/snakepacker/python/3.13 AS runner -ENV WORKDIR="/home" -ENV PATH="${WORKDIR}/.venv/bin:${PATH}" -WORKDIR ${WORKDIR} +RUN uv export --frozen --project deps/mapchete-hub-cli >> raw.txt && \ + grep '==' raw.txt | \ + grep -v "mapchete" | \ + sed 's/[[:space:];\\].*//' | sort -V | \ + awk -F'==' '{a[$1]=$2} END {for (i in a) print i"=="a[i]}' | sort > requirements.txt + +RUN uv init --name factory-build --lib . && \ + uv add -c requirements.txt ./deps/mapchete-hub-cli -# copy venv from builder -COPY --from=builder "${WORKDIR}/.venv" "${WORKDIR}/.venv" +# Generate the fingerprint based on the resolved lockfile +# Combine the lockfile hash AND a hash of the source code +# to ensure code-only changes are captured +RUN sha256sum uv.lock > /app/combined_hash && \ + # 1. Find all relevant files (py, toml, yaml) + # 2. Exclude .git and hidden directories + # 3. Use 'cat' to hash content only (ignoring mtime) + # 4. Sort to ensure identical order + find deps/ -type f \( -name "*.py" -o -name "*.toml" -o -name "*.yaml" \) \ + -not -path '*/.*' | sort | xargs sha256sum >> /app/combined_hash && \ + # Create final fingerprint + sha256sum /app/combined_hash | cut -d' ' -f1 > /app/uv_fingerprint -ENTRYPOINT [ "mhub" ] + +FROM ghcr.io/osgeo/gdal:ubuntu-small-3.12.0 + +ARG IMAGE_FINGERPRINT=unknown + +COPY --from=ghcr.io/astral-sh/uv:latest /uv /uvx /bin/ + +WORKDIR /app + +COPY --from=builder /app/.venv /app/.venv +COPY --from=builder /app/deps /app/deps +COPY --from=builder /app/uv_fingerprint /app/uv_fingerprint + +COPY . . + +LABEL org.mapchete.image_fingerprint=$IMAGE_FINGERPRINT +LABEL org.mapchete.uv_lock_hash=$IMAGE_FINGERPRINT + +ENV PATH="/app/.venv/bin:$PATH" +ENTRYPOINT ["mhub"] \ No newline at end of file diff --git a/mhub-cli-light/requirements.in b/mhub-cli-light/requirements.in deleted file mode 100644 index 2832227..0000000 --- a/mhub-cli-light/requirements.in +++ /dev/null @@ -1 +0,0 @@ -mapchete-hub-cli @ git+https://github.com/mapchete/mapchete-hub-cli.git@2025.9.0 \ No newline at end of file diff --git a/versions.yml b/versions.yml new file mode 100644 index 0000000..2b0011a --- /dev/null +++ b/versions.yml @@ -0,0 +1,10 @@ +MAPCHETE_REF_RELEASE: 2026.2.1 +MAPCHETE_EO_REF_RELEASE: 2026.2.0 +MAPCHETE_HUB_REF_RELEASE: 2026.2.1 +MAPCHETE_HUB_CLI_REF_RELEASE: 2025.9.0 + + +MAPCHETE_REF_ROLLING: main +MAPCHETE_EO_REF_ROLLING: main +MAPCHETE_HUB_REF_ROLLING: main +MAPCHETE_HUB_CLI_REF_ROLLING: main