diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 16d98a7..6311439 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,93 +7,117 @@ on: workflow_dispatch: inputs: tag: - description: 'Workflow to manually rebuild the image for a specific tag' + description: 'Build main source using an existing vMAJOR.MINOR.PATCH image version' required: true + type: string + +permissions: + contents: read jobs: - precheck: - runs-on: ubuntu-latest - outputs: - harbor_secret_set: ${{ steps.check_harbor_secret.outputs.harbor_secret_set }} + ci: + name: CI + if: github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && github.ref != 'refs/heads/main') + runs-on: example-self-hosted-saas-app-pr + concurrency: + group: example-self-hosted-saas-app-pr-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true steps: - name: Checkout uses: actions/checkout@v3 with: - fetch-depth: 0 - fetch-tags: true - - name: Check Harbor Secret - id: check_harbor_secret - run: | - if [ -z "${{ secrets.HARBOR_SECRET }}" ]; then - echo "::error::HARBOR_SECRET is not set.\nTo set it:" - echo "1. Go to your repository on GitHub." - echo "2. Click on 'Settings' > 'Secrets and variables' > 'Actions'." - echo "3. Click 'New repository secret'." - echo "4. Name it HARBOR_SECRET and paste your Harbor password or token as the value." - echo "5. Save the secret." - echo "harbor_secret_set=false" >> $GITHUB_OUTPUT - exit 1 - else - echo "HARBOR_SECRET is set." - echo "harbor_secret_set=true" >> $GITHUB_OUTPUT - fi + persist-credentials: false + - name: Set up Buildx (per-job rootless BuildKit) + uses: docker/setup-buildx-action@v3 + with: + driver: remote + endpoint: tcp://127.0.0.1:1234 + - name: Build image without publishing + uses: docker/build-push-action@v6 + with: + context: . + push: false - build: - needs: precheck - if: needs.precheck.outputs.harbor_secret_set == 'true' - runs-on: self-hosted + publish: + name: Publish + if: github.ref == 'refs/heads/main' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch') + runs-on: example-self-hosted-saas-app-main + concurrency: + group: example-self-hosted-saas-app-publish + cancel-in-progress: false + queue: max + permissions: + contents: write env: IMAGE_NAME: harbor.local/example-self-hosted-saas-app/example-self-hosted-saas-app + REBUILD_TAG: ${{ inputs.tag }} steps: - name: Checkout uses: actions/checkout@v3 with: + # Manual rebuilds always rebuild current main. A push must build the + # exact commit it was triggered for: publication is serialized, so a + # queued run would otherwise pick up a later commit and tag it. + ref: ${{ github.event_name == 'workflow_dispatch' && 'main' || github.sha }} fetch-depth: 0 fetch-tags: true - - name: Checkout tag if provided - if: github.event.inputs.tag != '' + - name: Validate existing image version tag (build main source) + if: github.event_name == 'workflow_dispatch' + shell: bash run: | - git checkout ${{ github.event.inputs.tag }} - - name: Set up Buildx (remote) + if [[ ! "$REBUILD_TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + echo '::error::Rebuild tag must be vMAJOR.MINOR.PATCH.' + exit 1 + fi + git show-ref --verify --quiet "refs/tags/$REBUILD_TAG" || { + echo '::error::Rebuild tag must already exist in the source repository.' + exit 1 + } + - name: Set up Buildx (shared BuildKit) uses: docker/setup-buildx-action@v3 with: driver: remote endpoint: tcp://buildkitd.buildkit:1234 - name: Get and increment latest tag - id: semver + shell: bash run: | - if [ -n "${{ github.event.inputs.tag }}" ]; then - NEW_TAG="${{ github.event.inputs.tag }}" + if [ "$GITHUB_EVENT_NAME" = workflow_dispatch ]; then + NEW_TAG="$REBUILD_TAG" echo "Using provided tag: $NEW_TAG" else - LATEST_TAG=$(git tag --list --sort=-v:refname | grep -E '^v[0-9]+\.[0-9]+\.[0-9]+$' | head -n1) + LATEST_TAG="" + while IFS= read -r TAG; do + if [[ "$TAG" =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then + LATEST_TAG="$TAG" + break + fi + done < <(git tag --list --sort=-v:refname) if [ -z "$LATEST_TAG" ]; then NEW_TAG="v1.0.0" else - # Remove 'v' prefix for parsing LATEST_TAG_NUMERIC=${LATEST_TAG#v} IFS='.' read -r MAJOR MINOR PATCH <<< "$LATEST_TAG_NUMERIC" - PATCH=$((PATCH + 1)) + PATCH=$((10#$PATCH + 1)) NEW_TAG="v$MAJOR.$MINOR.$PATCH" fi echo "Latest tag: $LATEST_TAG" echo "New tag: $NEW_TAG" fi - echo "NEW_TAG=$NEW_TAG" >> $GITHUB_ENV + echo "NEW_TAG=$NEW_TAG" >> "$GITHUB_ENV" - name: Build and push image uses: docker/build-push-action@v6 with: context: . - push: ${{ (github.event_name == 'push' || github.event_name == 'workflow_dispatch') && github.ref == 'refs/heads/main' }} + push: true tags: | ${{ env.IMAGE_NAME }}:${{ env.NEW_TAG }} ${{ env.IMAGE_NAME }}:latest cache-from: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache cache-to: type=registry,ref=${{ env.IMAGE_NAME }}:buildcache,mode=min - name: Create and push new git tag - if: github.event_name == 'push' && github.ref == 'refs/heads/main' + if: github.event_name == 'push' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - git tag $NEW_TAG - git push origin $NEW_TAG + git tag "$NEW_TAG" + git push origin "refs/tags/$NEW_TAG" diff --git a/README.md b/README.md index 1f92ea8..f7dbadb 100644 --- a/README.md +++ b/README.md @@ -127,6 +127,16 @@ spec: - Create an ArgoCD Application resource pointing to this repository and the `k8s/` directory. - ArgoCD will automatically sync the changes to your cluster. +## Repository CI and Publication + +`.github/workflows/build.yml` runs in this source repository with the normal `GITHUB_TOKEN`; it requires no paid runner groups or private CI repository. + +- Pull requests run **CI** on `example-self-hosted-saas-app-pr`, using a tokenless Kubernetes service account and per-job rootless BuildKit at `tcp://127.0.0.1:1234`. They build without publishing, registry cache writes, source version allocation, or Kubernetes API access. +- Main pushes run **Publish** on `example-self-hosted-saas-app-main`, using shared BuildKit and infrastructure-mounted Harbor authentication. Publication writes version/latest images and buildcache, then creates the next patch source tag. +- Manual publication must be dispatched from **main**. The required existing `vMAJOR.MINOR.PATCH` tag selects only the image version: the build always checks out **main**, does not check out that tag, and creates no source tag. Automatic and manual publication share one serialized queue. +- Dispatching manually from a **non-main** branch runs **CI** on the `-pr` pool instead; the `tag` input is ignored on that path, and GitHub still requires a value because the workflow declares it as required. +- Both runner pools are repository-scoped in the `example-self-hosted-saas-app` namespace. Public-repository outside contributors require approval **before the first self-hosted run**. Approving an outside workflow trusts its code to execute in the cluster; namespaces and runner labels are operational routing, not authorization boundaries. + ## Integration with Infrastructure Your application will automatically benefit from: