diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml index f523997..16683fa 100644 --- a/.gitea/workflows/ci.yml +++ b/.gitea/workflows/ci.yml @@ -1 +1,188 @@ -bmFtZTogQ0kKCm9uOgogIHB1c2g6CiAgICBicmFuY2hlczogW21haW4sIGRldiwgdWF0XQogIHB1bGxfcmVxdWVzdDoKICAgIGJyYW5jaGVzOiBbbWFpbiwgZGV2LCB1YXRdCiAgd29ya2Zsb3dfZGlzcGF0Y2g6CiAgICBpbnB1dHM6CiAgICAgIHJlZjoKICAgICAgICBkZXNjcmlwdGlvbjogIkJyYW5jaCBvciByZWYgdG8gcnVuIENJIGFnYWluc3QiCiAgICAgICAgcmVxdWlyZWQ6IGZhbHNlCiAgICAgICAgZGVmYXVsdDogIm1haW4iCgpqb2JzOgogIGxpbnQtdHlwZWNoZWNrOgogICAgbmFtZTogTGludCAmIFR5cGVjaGVjawogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgc3RlcHM6CiAgICAgIC0gdXNlczogYWN0aW9ucy9jaGVja291dEB2NAoKICAgICAgLSB1c2VzOiBwbnBtL2FjdGlvbi1zZXR1cEB2NAogICAgICAgIHdpdGg6CiAgICAgICAgICB2ZXJzaW9uOiAnOS4xNS40JwoKICAgICAgLSB1c2VzOiBhY3Rpb25zL3NldHVwLW5vZGVAdjQKICAgICAgICB3aXRoOgogICAgICAgICAgbm9kZS12ZXJzaW9uOiAyMgogICAgICAgICAgY2FjaGU6IHBucG0KCiAgICAgIC0gbmFtZTogSW5zdGFsbCBkZXBlbmRlbmNpZXMKICAgICAgICBydW46IHBucG0gaW5zdGFsbCAtLWZyb3plbi1sb2NrZmlsZQoKICAgICAgLSBuYW1lOiBUeXBlY2hlY2sKICAgICAgICBydW46IHwKICAgICAgICAgIHBucG0gcnVuIHR5cGVjaGVjawogICAgICAgICAgcG5wbSAtLWZpbHRlciBAZ3Jvb21ib29rL2RiIHR5cGVjaGVjawoKICAgICAgLSBuYW1lOiBMaW50CiAgICAgICAgcnVuOiBwbnBtIHJ1biBsaW50CgogIHRlc3Q6CiAgICBuYW1lOiBUZXN0CiAgICBydW5zLW9uOiB1YnVudHUtbGF0ZXN0CiAgICBzdGVwczoKICAgICAgLSB1c2VzOiBhY3Rpb25zL2NoZWNrb3V0QHY0CgogICAgICAtIHVzZXM6IHBucG0vYWN0aW9uLXNldHVwQHY0CiAgICAgICAgd2l0aDoKICAgICAgICAgIHZlcnNpb246ICc5LjE1LjQnCgogICAgICAtIHVzZXM6IGFjdGlvbnMvc2V0dXAtbm9kZUB2NAogICAgICAgIHdpdGg6CiAgICAgICAgICBub2RlLXZlcnNpb246IDIyCiAgICAgICAgICBjYWNoZTogcG5wbQoKICAgICAgLSBuYW1lOiBJbnN0YWxsIGRlcGVuZGVuY2llcwogICAgICAgIHJ1bjogcG5wbSBpbnN0YWxsIC0tZnJvemVuLWxvY2tmaWxlCgogICAgICAtIG5hbWU6IFJ1biB0ZXN0cwogICAgICAgIHJ1bjogcG5wbSBydW4gdGVzdAoKICBkb2NrZXI6CiAgICBuYW1lOiBCdWlsZCAmIFB1c2ggRG9ja2VyIEltYWdlcwogICAgcnVucy1vbjogdWJ1bnR1LWxhdGVzdAogICAgbmVlZHM6IFtsaW50LXR5cGVjaGVjaywgdGVzdF0KICAgIHN0ZXBzOgogICAgICAtIHVzZXM6IGFjdGlvbnMvY2hlY2tvdXRAdjQKCiAgICAgIC0gbmFtZTogR2VuZXJhdGUgaW1hZ2UgdGFnCiAgICAgICAgaWQ6IHZlcnNpb24KICAgICAgICBydW46IHwKICAgICAgICAgIGlmIFsgIiR7eyBnaXRodWIuZXZlbnRfbmFtZSB9fSIgPSAicHVsbF9yZXF1ZXN0IiBdOyB0aGVuCiAgICAgICAgICAgIFRBRz0icHItJHt7IGdpdGh1Yi5ldmVudC5wdWxsX3JlcXVlc3QubnVtYmVyIH19LSR7R0lUSFVCX1NIQTo6N30iCiAgICAgICAgICBlbHNlCiAgICAgICAgICAgIFRBRz0iJChkYXRlIC11ICslWS4lbS4lZCktJHtHSVRIVUJfU0hBOjo3fSIKICAgICAgICAgIGZpCiAgICAgICAgICBlY2hvICJ0YWc9JFRBRyIgPj4gIiRHSVRIVUJfT1VUUFVUIgogICAgICAgICAgZWNobyAiSW1hZ2UgdGFnOiAkVEFHIgoKICAgICAgLSBuYW1lOiBTZXQgdXAgRG9ja2VyIEJ1aWxkeAogICAgICAgIHVzZXM6IGRvY2tlci9zZXR1cC1idWlsZHgtYWN0aW9uQHYzCiAgICAgICAgd2l0aDoKICAgICAgICAgIGRyaXZlci1vcHRzOiBuZXR3b3JrPWhvc3QKCiAgICAgIC0gbmFtZTogTG9nIGluIHRvIEdpdGVhIENvbnRhaW5lciBSZWdpc3RyeQogICAgICAgIHVzZXM6IGRvY2tlci9sb2dpbi1hY3Rpb25AdjMKICAgICAgICB3aXRoOgogICAgICAgICAgcmVnaXN0cnk6IGdpdC5mYXJoLm5ldAogICAgICAgICAgdXNlcm5hbWU6ICR7eyBnaXRlYS5hY3RvciB9fQogICAgICAgICAgcGFzc3dvcmQ6ICR7eyBzZWNyZXRzLlJFR0lTVFJZX1RPS0VOIH19CgogICAgICAtIG5hbWU6IEJ1aWxkIGFuZCBwdXNoIEFQSSBpbWFnZQogICAgICAgIHVzZXM6IGRvY2tlci9idWlsZC1wdXNoLWFjdGlvbkB2NgogICAgICAgIHdpdGg6CiAgICAgICAgICBwcm92ZW5hbmNlOiBmYWxzZQogICAgICAgICAgY29udGV4dDogLgogICAgICAgICAgZmlsZTogRG9ja2VyZmlsZQogICAgICAgICAgdGFyZ2V0OiBydW5uZXIKICAgICAgICAgIHB1c2g6IHRydWUKICAgICAgICAgIHRhZ3M6IHwKICAgICAgICAgICAgZ2l0LmZhcmgubmV0L2dyb29tYm9vay9hcGk6JHt7IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy50YWcgfX0KICAgICAgICAgICAgJHt7IGdpdGh1Yi5yZWYgPT0gJ3JlZnMvaGVhZHMvbWFpbicgJiYgJ2dpdC5mYXJoLm5ldC9ncm9vbWJvb2svYXBpOmxhdGVzdCcgfHwgJycgfX0KICAgICAgICAgIGNhY2hlLWZyb206IHR5cGU9cmVnaXN0cnkscmVmPWdpdC5mYXJoLm5ldC9ncm9vbWJvb2svY2FjaGU6YXBpCiAgICAgICAgICBjYWNoZS10bzogdHlwZT1yZWdpc3RyeSxyZWY9Z2l0LmZhcmgubmV0L2dyb29tYm9vay9jYWNoZTphcGksbW9kZT1tYXgsaWdub3JlLWVycm9yPXRydWUKCiAgICAgIC0gbmFtZTogQnVpbGQgYW5kIHB1c2ggTWlncmF0ZSBpbWFnZQogICAgICAgIHVzZXM6IGRvY2tlci9idWlsZC1wdXNoLWFjdGlvbkB2NgogICAgICAgIHdpdGg6CiAgICAgICAgICBwcm92ZW5hbmNlOiBmYWxzZQogICAgICAgICAgY29udGV4dDogLgogICAgICAgICAgZmlsZTogRG9ja2VyZmlsZQogICAgICAgICAgdGFyZ2V0OiBtaWdyYXRlCiAgICAgICAgICBwdXNoOiB0cnVlCiAgICAgICAgICB0YWdzOiB8CiAgICAgICAgICAgIGdpdC5mYXJoLm5ldC9ncm9vbWJvb2svbWlncmF0ZToke3sgc3RlcHMudmVyc2lvbi5vdXRwdWRzLnRhZyB9fQogICAgICAgICAgICAke3sgZ2l0aHViLnJlZiA9PSAncmVmcy9oZWFkcy9tYWluJyAmJiAnZ2l0LmZhcmgubmV0L2dyb29tYm9vay9taWdyYXRlOmxhdGVzdCcgfHwgJycgfX0KICAgICAgICAgIGNhY2hlLWZyb206IHR5cGU9cmVnaXN0cnkscmVmPWdpdC5mYXJoLm5ldC9ncm9vbWJvb2svY2FjaGU6bWlncmF0ZQogICAgICAgICAgY2FjaGUtdG86IHR5cGU9cmVnaXN0cnkscmVmPWdpdC5mYXJoLm5ldC9ncm9vbWJvb2svY2FjaGU6bWlncmF0ZSxtb2RlPW1heCxpZ25vcmUtZXJyb3I9dHJ1ZQoKICAgICAgLSBuYW1lOiBTbW9rZSB0ZXN0IG1pZ3JhdGUgaW1hZ2UgKGJsYWNraG9sZSBucG1qcy5vcmcpCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBzZXQgLWV1byBwaXBlZmFpbAogICAgICAgICAgSU1BR0U9ImdpdC5mYXJoLm5ldC9ncm9vbWJvb2svbWlncmF0ZToke3sgc3RlcHMudmVyc2lvbi5vdXRwdXRzLnRhZyB9fSIKICAgICAgICAgIGRvY2tlciBwdWxsICIkSU1BR0UiCiAgICAgICAgICBkb2NrZXIgcnVuIC0tcm0gXAogICAgICAgICAgICAtLWFkZC1ob3N0IHJlZ2lzdHJ5Lm5wbWpzLm9yZzoxMjcuMC4wLjEgXAogICAgICAgICAgICAtLWVudHJ5cG9pbnQ9IiIgXAogICAgICAgICAgICAiJElNQUdFIiBcCiAgICAgICAgICAgIHBucG0gLS12ZXJzaW9uCgogICAgICAtIG5hbWU6IEJ1aWxkIGFuZCBwdXNoIFNlZWQgaW1hZ2UKICAgICAgICB1c2VzOiBkb2NrZXIvYnVpbGQtcHVzaC1hY3Rpb25AdjYKICAgICAgICB3aXRoOgogICAgICAgICAgcHJvdmVuYW5jZTogZmFsc2UKICAgICAgICAgIGNvbnRleHQ6IC4KICAgICAgICAgIGZpbGU6IERvY2tlcmZpbGUKICAgICAgICAgIHRhcmdldDogc2VlZAogICAgICAgICAgcHVzaDogdHJ1ZQogICAgICAgICAgdGFnczogfAogICAgICAgICAgICBnaXQuZmFyaC5uZXQvZ3Jvb21ib29rL3NlZWQ6JHt7IHN0ZXBzLnZlcnNpb24ub3V0cHV0cy50YWcgfX0KICAgICAgICAgICAgJHt7IGdpdGh1Yi5yZWYgPT0gJ3JlZnMvaGVhZHMvbWFpbicgJiYgJ2dpdC5mYXJoLm5ldC9ncm9vbWJvb2svc2VlZDpsYXRlc3QnIHx8ICcnIH19CiAgICAgICAgICBjYWNoZS1mcm9tOiB0eXBlPXJlZ2lzdHJ5LHJlZj1naXQuZmFyaC5uZXQvZ3Jvb21ib29rL2NhY2hlOnNlZWQKICAgICAgICAgIGNhY2hlLXRvOiB0eXBlPXJlZ2lzdHJ5LHJlZj1naXQuZmFyaC5uZXQvZ3Jvb21ib29rL2NhY2hlOnNlZWQsbW9kZT1tYXgsaWdub3JlLWVycm9yPXRydWUKCiAgICAgIC0gbmFtZTogQnVpbGQgYW5kIHB1c2ggUmVzZXQgaW1hZ2UKICAgICAgICB1c2VzOiBkb2NrZXIvYnVpbGQtcHVzaC1hY3Rpb25AdjYKICAgICAgICB3aXRoOgogICAgICAgICAgcHJvdmVuYW5jZTogZmFsc2UKICAgICAgICAgIGNvbnRleHQ6IC4KICAgICAgICAgIGZpbGU6IERvY2tlcmZpbGUKICAgICAgICAgIHRhcmdldDogcmVzZXQKICAgICAgICAgIHB1c2g6IHRydWUKICAgICAgICAgIHRhZ3M6IHwKICAgICAgICAgICAgZ2l0LmZhcmgubmV0L2dyb29tYm9vay9yZXNldDoke3sgc3RlcHMudmVyc2lvbi5vdXRwdXRzLnRhZyB9fQogICAgICAgICAgICAke3sgZ2l0aHViLnJlZiA9PSAncmVmcy9oZWFkcy9tYWluJyAmJiAnZ2l0LmZhcmgubmV0L2dyb29tYm9vay9yZXNldDpsYXRlc3QnIHx8ICcnIH19CiAgICAgICAgICBjYWNoZS1mcm9tOiB0eXBlPXJlZ2lzdHJ5LHJlZj1naXQuZmFyaC5uZXQvZ3Jvb21ib29rL2NhY2hlOnJlc2V0CiAgICAgICAgICBjYWNoZS10bzogdHlwZT1yZWdpc3RyeSxyZWY9Z2l0LmZhcmgubmV0L2dyb29tYm9vay9jYWNoZTpyZXNldCxtb2RlPW1heCxpZ25vcmUtZXJyb3I9dHJ1ZQoKICAgICAgLSBuYW1lOiBTbW9rZSB0ZXN0IHNlZWQgaW1hZ2UgKGJsYWNraG9sZSBucG1qcy5vcmcpCiAgICAgICAgcnVuOiB8CiAgICAgICAgICBzZXQgLWV1byBwaXBlZmFpbAogICAgICAgICAgSU1BR0U9ImdpdC5mYXJoLm5ldC9ncm9vbWJvb2svc2VlZDoke3sgc3RlcHMudmVyc2lvbi5vdXRwdXRzLnRhZyB9fSIKICAgICAgICAgIGRvY2tlciBwdWxsICIkSU1BR0UiCiAgICAgICAgICAjIEdSTy0xOTg1OiBwbnBtIG11c3QgYmUgYSByZWFsIGJpbmFyeSwgbm90IGEgQ29yZXBhY2sgc2hpbSwgYW5kIG11c3QKICAgICAgICAgICMgbm90IHRyeSB0byByZWFjaCByZWdpc3RyeS5ucG1qcy5vcmcgb24gaW52b2NhdGlvbi4KICAgICAgICAgIGRvY2tlciBydW4gLS1ybSBcCiAgICAgICAgICAgIC0tYWRkLWhvc3QgcmVnaXN0cnkubnBtanMub3JnOjEyNy4wLjAuMSBcCiAgICAgICAgICAgIC0tZW50cnlwb2ludD0iIiBcCiAgICAgICAgICAgICIkSU1BR0UiIFwKICAgICAgICAgICAgc2ggLWMgJ3NldCAtZTsgdGVzdCAiJCh3aGljaCBwbnBtKSIgPSAiL3Vzci9sb2NhbC9iaW4vcG5wbSI7IHBucG0gLS12ZXJzaW9uJwogICAgICAgICAgZWNobyAic2VlZCBpbWFnZTogcG5wbSByZXNvbHZlcyB0byAvdXNyL2xvY2FsL2Jpbi9wbnBtIGFuZCBydW5zIG9mZmxpbmUg4pyTIgoKICAgICAgLSBuYW1lOiBTbW9rZSB0ZXN0IHJlc2V0IGltYWdlIChibGFja2hvbGUgbnBtanMub3JnKQogICAgICAgIHJ1bjogfAogICAgICAgICAgc2V0IC1ldW8gcGlwZWZhaWwKICAgICAgICAgIElNQUdFPSJnaXQuZmFyaC5uZXQvZ3Jvb21ib29rL3Jlc2V0OiR7eyBzdGVwcy52ZXJzaW9uLm91dHB1dHMudGFnIH19IgogICAgICAgICAgZG9ja2VyIHB1bGwgIiRJTUFHRSIKICAgICAgICAgICMgR1JPLTE5ODU6IHBucG0gbXVzdCBiZSBhIHJlYWwgYmluYXJ5LCBub3QgYSBDb3JlcGFjayBzaGltLCBhbmQgbXVzdAogICAgICAgICAgIyBub3QgdHJ5IHRvIHJlYWNoIHJlZ2lzdHJ5Lm5wbWpzLm9yZyBvbiBpbnZvY2F0aW9uLiBWYWxpZGF0ZXMgdGhlCiAgICAgICAgICAjIGhhcmQgcmVxdWlyZW1lbnQgZnJvbSB0aGUgaXNzdWU6IHJlc2V0IHJ1bnMgb2ZmbGluZS4KICAgICAgICAgIGRvY2tlciBydW4gLS1ybSBcCiAgICAgICAgICAgIC0tYWRkLWhvc3QgcmVnaXN0cnkubnBtanMub3JnOjEyNy4wLjAuMSBcCiAgICAgICAgICAgIC0tZW50cnlwb2ludD0iIiBcCiAgICAgICAgICAgICIkSU1BR0UiIFwKICAgICAgICAgICAgc2ggLWMgJ3NldCAtZTsgdGVzdCAiJCh3aGljaCBwbnBtKSIgPSAiL3Vzci9sb2NhbC9iaW4vcG5wbSI7IGVjaG8gIkhPTUU9JEhPTUUiOyBwbnBtIC0tdmVyc2lvbicKICAgICAgICAgIGVjaG8gInJlc2V0IGltYWdlOiBwbnBtIHJlc29sdmVzIHRvIC91c3IvbG9jYWwvYmluL3BucG0sIEhPTUU9L3RtcCwgcnVucyBvZmZsaW5lIOKckyIK \ No newline at end of file +name: CI + +on: + push: + branches: [main, dev, uat] + pull_request: + branches: [main, dev, uat] + workflow_dispatch: + inputs: + ref: + description: "Branch or ref to run CI against" + required: false + default: "main" + +jobs: + lint-typecheck: + name: Lint & Typecheck + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: '9.15.4' + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Typecheck + run: | + pnpm run typecheck + pnpm --filter @groombook/db typecheck + + - name: Lint + run: pnpm run lint + + test: + name: Test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: pnpm/action-setup@v4 + with: + version: '9.15.4' + + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: pnpm + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Run tests + run: pnpm run test + + docker: + name: Build & Push Docker Images + runs-on: ubuntu-latest + needs: [lint-typecheck, test] + steps: + - uses: actions/checkout@v4 + + - name: Generate image tag + id: version + run: | + if [ "${{ github.event_name }}" = "pull_request" ]; then + TAG="pr-${{ github.event.pull_request.number }}-${GITHUB_SHA::7}" + else + TAG="$(date -u +%Y.%m.%d)-${GITHUB_SHA::7}" + fi + echo "tag=$TAG" >> "$GITHUB_OUTPUT" + echo "Image tag: $TAG" + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + with: + driver-opts: network=host + + - name: Log in to Gitea Container Registry + uses: docker/login-action@v3 + with: + registry: git.farh.net + username: ${{ gitea.actor }} + password: ${{ secrets.REGISTRY_TOKEN }} + + - name: Build and push API image + uses: docker/build-push-action@v6 + with: + provenance: false + context: . + file: Dockerfile + target: runner + push: true + tags: | + git.farh.net/groombook/api:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/api:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:api + cache-to: type=registry,ref=git.farh.net/groombook/cache:api,mode=max,ignore-error=true + + - name: Build and push Migrate image + uses: docker/build-push-action@v6 + with: + provenance: false + context: . + file: Dockerfile + target: migrate + push: true + tags: | + git.farh.net/groombook/migrate:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/migrate:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:migrate + cache-to: type=registry,ref=git.farh.net/groombook/cache:migrate,mode=max,ignore-error=true + + - name: Smoke test migrate image (blackhole npmjs.org) + run: | + set -euo pipefail + IMAGE="git.farh.net/groombook/migrate:${{ steps.version.outputs.tag }}" + docker pull "$IMAGE" + docker run --rm \ + --add-host registry.npmjs.org:127.0.0.1 \ + --entrypoint="" \ + "$IMAGE" \ + pnpm --version + + - name: Build and push Seed image + uses: docker/build-push-action@v6 + with: + provenance: false + context: . + file: Dockerfile + target: seed + push: true + tags: | + git.farh.net/groombook/seed:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/seed:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:seed + cache-to: type=registry,ref=git.farh.net/groombook/cache:seed,mode=max,ignore-error=true + + - name: Build and push Reset image + uses: docker/build-push-action@v6 + with: + provenance: false + context: . + file: Dockerfile + target: reset + push: true + tags: | + git.farh.net/groombook/reset:${{ steps.version.outputs.tag }} + ${{ github.ref == 'refs/heads/main' && 'git.farh.net/groombook/reset:latest' || '' }} + cache-from: type=registry,ref=git.farh.net/groombook/cache:reset + cache-to: type=registry,ref=git.farh.net/groombook/cache:reset,mode=max,ignore-error=true + + - name: Smoke test seed image (blackhole npmjs.org) + run: | + set -euo pipefail + IMAGE="git.farh.net/groombook/seed:${{ steps.version.outputs.tag }}" + docker pull "$IMAGE" + # GRO-1985: pnpm must be a real binary, not a Corepack shim, and must + # not try to reach registry.npmjs.org on invocation. + docker run --rm \ + --add-host registry.npmjs.org:127.0.0.1 \ + --entrypoint="" \ + "$IMAGE" \ + sh -c 'set -e; test "$(which pnpm)" = "/usr/local/bin/pnpm"; pnpm --version' + echo "seed image: pnpm resolves to /usr/local/bin/pnpm and runs offline ✓" + + - name: Smoke test reset image (blackhole npmjs.org) + run: | + set -euo pipefail + IMAGE="git.farh.net/groombook/reset:${{ steps.version.outputs.tag }}" + docker pull "$IMAGE" + # GRO-1985: pnpm must be a real binary, not a Corepack shim, and must + # not try to reach registry.npmjs.org on invocation. Validates the + # hard requirement from the issue: reset runs offline. + docker run --rm \ + --add-host registry.npmjs.org:127.0.0.1 \ + --entrypoint="" \ + "$IMAGE" \ + sh -c 'set -e; test "$(which pnpm)" = "/usr/local/bin/pnpm"; echo "HOME=$HOME"; pnpm --version' + echo "reset image: pnpm resolves to /usr/local/bin/pnpm, HOME=/tmp, runs offline ✓" +