diff --git a/.gitea/workflows/build.yml b/.gitea/workflows/build.yml new file mode 100644 index 0000000..fee713b --- /dev/null +++ b/.gitea/workflows/build.yml @@ -0,0 +1,101 @@ +name: Build and Push + +on: + push: + branches: [main] + # Don't rebuild on doc-only or CI-config-only changes + paths-ignore: + - 'README.md' + - '.gitea/**' + - 'deploy/**' + workflow_dispatch: + +env: + REGISTRY: registry.c5ai.ch + IMAGE: pieced/pieced-portal + +jobs: + build: + # 'self-hosted' matches the label our act_runner registers with. + # 'ubuntu-latest' would work too because we configure both labels in the + # runner config, but self-hosted makes intent explicit. + runs-on: ubuntu-latest + + outputs: + version: ${{ steps.version.outputs.version }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Determine next patch version + id: version + # Reads tags from the registry's OCI Distribution v2 API, filters to + # strict semver (skips 'latest', 'dev', '-dirty', etc.), picks the + # highest with version-sort, and bumps the patch component. If nothing + # numeric exists yet (fresh registry), starts at 0.1.0. + run: | + set -euo pipefail + tags_json=$(curl -sf -u "${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_PASSWORD }}" \ + "https://${REGISTRY}/v2/${IMAGE}/tags/list") + highest=$(echo "$tags_json" \ + | jq -r '.tags // [] | .[]' \ + | grep -E '^[0-9]+\.[0-9]+\.[0-9]+$' \ + | sort -V \ + | tail -n1 || true) + if [ -z "$highest" ]; then + next="0.1.0" + echo "No semver tags found — starting at $next" + else + major=$(echo "$highest" | cut -d. -f1) + minor=$(echo "$highest" | cut -d. -f2) + patch=$(echo "$highest" | cut -d. -f3) + next="${major}.${minor}.$((patch + 1))" + echo "Highest existing: $highest → next: $next" + fi + echo "version=${next}" >> "$GITHUB_OUTPUT" + + - name: Login to registry + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" \ + | docker login "${REGISTRY}" -u "${{ secrets.REGISTRY_USERNAME }}" --password-stdin + + - name: Build and push image + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + docker build \ + --pull \ + -t "${REGISTRY}/${IMAGE}:${VERSION}" \ + -t "${REGISTRY}/${IMAGE}:latest" \ + . + docker push "${REGISTRY}/${IMAGE}:${VERSION}" + docker push "${REGISTRY}/${IMAGE}:latest" + + - name: Tag git commit with version + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + set -euo pipefail + git config user.name "pieced-ci" + git config user.email "ci@pieced.ch" + git tag -a "v${VERSION}" -m "Release ${VERSION}" + # Use CI_TOKEN explicitly so we can push a tag (the workflow's + # default token may or may not have push scope depending on Gitea + # actions config — explicit token avoids ambiguity). + git push \ + "https://oauth2:${{ secrets.CI_TOKEN }}@git.c5ai.ch/pieced/pieced-portal.git" \ + "v${VERSION}" + + - name: Summary + env: + VERSION: ${{ steps.version.outputs.version }} + run: | + { + echo "## Build complete: ${VERSION}" + echo + echo "**Image:** \`${REGISTRY}/${IMAGE}:${VERSION}\`" + echo + echo "Run the **Deploy to GitOps** workflow to roll this version out." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml new file mode 100644 index 0000000..1cd456f --- /dev/null +++ b/.gitea/workflows/deploy.yml @@ -0,0 +1,94 @@ +name: Deploy to GitOps + +# Manually triggered. Bumps the image tag in pieced-gitops so ArgoCD rolls +# the new version out. Does not build anything itself — the build workflow +# is the only thing that creates and pushes images. +on: + workflow_dispatch: + inputs: + version: + description: 'Version to deploy (e.g. 0.1.5). Must already exist in the registry.' + required: true + type: string + +env: + REGISTRY: registry.c5ai.ch + IMAGE: pieced/pieced-portal + GITOPS_REPO: admin/pieced-gitops + GITOPS_FILE: apps/portal/deployment.yaml + +jobs: + deploy: + runs-on: ubuntu-latest + + steps: + - name: Verify image exists in registry + # Fail fast if the user typed a version that was never built. Catches + # typos before we touch the gitops repo. + run: | + set -euo pipefail + status=$(curl -sf -o /dev/null -w '%{http_code}' \ + -u "${{ secrets.REGISTRY_USERNAME }}:${{ secrets.REGISTRY_PASSWORD }}" \ + "https://${REGISTRY}/v2/${IMAGE}/manifests/${{ inputs.version }}" \ + || true) + if [ "$status" != "200" ]; then + echo "::error::Image ${REGISTRY}/${IMAGE}:${{ inputs.version }} not found (HTTP $status)" + exit 1 + fi + echo "Confirmed: ${REGISTRY}/${IMAGE}:${{ inputs.version }} exists." + + - name: Checkout pieced-gitops + uses: actions/checkout@v4 + with: + repository: ${{ env.GITOPS_REPO }} + token: ${{ secrets.CI_TOKEN }} + path: gitops + # We need history to commit + push back; default fetch-depth: 1 is fine + # for a single commit but force a clean shallow clone: + fetch-depth: 1 + + - name: Update image tag + working-directory: gitops + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + file="${GITOPS_FILE}" + if [ ! -f "$file" ]; then + echo "::error::$file not found in gitops repo" + exit 1 + fi + # Anchored to the full image path to avoid accidentally rewriting + # any unrelated 'image:' line that might appear later. + sed -i -E \ + "s|(image: ${REGISTRY}/${IMAGE}:)[^[:space:]]+|\1${VERSION}|" \ + "$file" + echo "--- diff ---" + git --no-pager diff "$file" || true + + - name: Commit and push + working-directory: gitops + env: + VERSION: ${{ inputs.version }} + run: | + set -euo pipefail + if git diff --quiet; then + echo "No changes — image tag was already ${VERSION}." + exit 0 + fi + git config user.name "pieced-ci" + git config user.email "ci@pieced.ch" + git add "${GITOPS_FILE}" + git commit -m "Bump pieced-portal to ${VERSION}" + git push + + - name: Summary + env: + VERSION: ${{ inputs.version }} + run: | + { + echo "## Deployed: pieced-portal ${VERSION}" + echo + echo "ArgoCD will sync within its refresh interval." + echo "Watch with: \`kubectl get app -n argocd portal -w\`" + } >> "$GITHUB_STEP_SUMMARY"