From e06e38214782af36a857b77488efb995973564af Mon Sep 17 00:00:00 2001 From: Patrick Niebeling Date: Thu, 13 Aug 2026 13:46:34 +0200 Subject: [PATCH] Merge release workflow into the main build workflow build-release.yml and build-main.yml ran identical steps (checkout, node, npm install, npm run build, make appstore) and differed only in where the package landed. Fold the v* tag trigger into build-main.yml and branch on gitea.ref in the publishing step: main keeps the rolling latest-main pre-release, tags get a normal release on the tag that was just pushed. Tag deletion is now guarded - only latest-main is recreated, since deleting a version tag would destroy the ref that triggered the run. A stale release is still removed in both cases so moved tags republish cleanly. Side effects: version builds are published as durable release assets with a stable URL instead of expiring workflow artifacts, which drops the last actions/upload-artifact usage and with it the @v3 pin forced by the GHES restriction; and the setup-php step goes away, since make appstore only shells out to mkdir/tar/rm. --- .gitea/workflows/build-main.yml | 53 +++++++++++++++++++++--------- .gitea/workflows/build-release.yml | 35 -------------------- CLAUDE.md | 23 ++++++++----- README.md | 2 +- 4 files changed, 54 insertions(+), 59 deletions(-) delete mode 100644 .gitea/workflows/build-release.yml diff --git a/.gitea/workflows/build-main.yml b/.gitea/workflows/build-main.yml index bc9c88b..3c0bb94 100644 --- a/.gitea/workflows/build-main.yml +++ b/.gitea/workflows/build-main.yml @@ -1,9 +1,11 @@ -name: Build main artifact +name: Build package on: push: branches: - main + tags: + - 'v*' jobs: package: @@ -24,34 +26,55 @@ jobs: - name: Package appstore artifact run: make appstore - - name: Publish rolling "latest-main" release + - name: Publish release env: GITEA_API: ${{ gitea.server_url }}/api/v1 REPO: ${{ gitea.repository }} TOKEN: ${{ secrets.RELEASE_TOKEN }} SHA: ${{ gitea.sha }} + REF: ${{ gitea.ref }} run: | set -e - TAG="latest-main" ASSET="build/artifacts/appstore/workflow_deck_automation.tar.gz" - # Drop any previous release+tag so the download URL always serves the newest build - curl -s -H "Authorization: token $TOKEN" \ - "$GITEA_API/repos/$REPO/releases/tags/$TAG" -o /tmp/old_release.json + case "$REF" in + refs/tags/*) + TAG="${REF#refs/tags/}" + RECREATE_TAG=false + PRERELEASE=false + TITLE="$TAG" + BODY="Release $TAG" + ;; + *) + TAG="latest-main" + RECREATE_TAG=true + PRERELEASE=true + TITLE="Latest main build" + BODY="Automatisch aus $SHA gebaut - nur zum schnellen Testen/Deployen, kein offizielles Release." + ;; + esac + + api() { curl -sS -H "Authorization: token $TOKEN" "$@"; } + + # Always drop an existing release for this tag first: the rolling tag is + # rebuilt on every push, and a moved version tag has to publish the new + # build instead of failing on a duplicate asset name. + api "$GITEA_API/repos/$REPO/releases/tags/$TAG" -o /tmp/old_release.json OLD_ID=$(node -e "try{const j=require('/tmp/old_release.json');console.log(j.id||'')}catch(e){console.log('')}") if [ -n "$OLD_ID" ]; then - curl -s -X DELETE -H "Authorization: token $TOKEN" \ - "$GITEA_API/repos/$REPO/releases/$OLD_ID" + api -X DELETE "$GITEA_API/repos/$REPO/releases/$OLD_ID" fi - curl -s -X DELETE -H "Authorization: token $TOKEN" \ - "$GITEA_API/repos/$REPO/tags/$TAG" > /dev/null || true - # Re-create the tag+release pointing at the commit that was just built - curl -s -X POST -H "Authorization: token $TOKEN" -H "Content-Type: application/json" \ - -d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"$SHA\",\"name\":\"Latest main build\",\"body\":\"Automatisch aus $SHA gebaut - nur zum schnellen Testen/Deployen, kein offizielles Release.\",\"prerelease\":true}" \ + # Only latest-main gets its tag recreated - a version tag was just pushed + # and must survive, otherwise this job would delete what triggered it. + if [ "$RECREATE_TAG" = true ]; then + api -X DELETE "$GITEA_API/repos/$REPO/tags/$TAG" > /dev/null || true + fi + + api -X POST -H "Content-Type: application/json" \ + -d "{\"tag_name\":\"$TAG\",\"target_commitish\":\"$SHA\",\"name\":\"$TITLE\",\"body\":\"$BODY\",\"prerelease\":$PRERELEASE}" \ "$GITEA_API/repos/$REPO/releases" -o /tmp/new_release.json NEW_ID=$(node -e "console.log(require('/tmp/new_release.json').id)") - curl -s -X POST -H "Authorization: token $TOKEN" \ - -F "attachment=@$ASSET" \ + api -X POST -F "attachment=@$ASSET" \ "$GITEA_API/repos/$REPO/releases/$NEW_ID/assets?name=workflow_deck_automation.tar.gz" diff --git a/.gitea/workflows/build-release.yml b/.gitea/workflows/build-release.yml deleted file mode 100644 index ecbdebf..0000000 --- a/.gitea/workflows/build-release.yml +++ /dev/null @@ -1,35 +0,0 @@ -name: Build release artifact - -on: - push: - tags: - - 'v*' - -jobs: - package: - runs-on: gitea-runner-server03 - steps: - - uses: actions/checkout@v7 - - - uses: actions/setup-node@v7 - with: - node-version: '24' - - - uses: shivammathur/setup-php@v2 - with: - php-version: '8.3' - coverage: none - - - name: Install JS dependencies - run: npm install - - - name: Build frontend - run: npm run build - - - name: Package appstore artifact - run: make appstore - - - uses: actions/upload-artifact@v3 - with: - name: workflow_deck_automation-${{ gitea.ref_name }} - path: build/artifacts/appstore/*.tar.gz diff --git a/CLAUDE.md b/CLAUDE.md index b8aa831..429d3a6 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,17 +47,24 @@ There is no meaningful way to test the Deck integration itself outside a real Ne 1. Bump `` in `appinfo/info.xml` and `version` in `package.json` (keep them in sync). 2. Commit, push to `main`, confirm the lint/phpunit pipelines are green. -3. `git tag vX.Y.Z && git push origin vX.Y.Z` — this triggers `.gitea/workflows/build-release.yml`, which builds the frontend and runs `make appstore`, producing `build/artifacts/appstore/workflow_deck_automation.tar.gz` as a **workflow artifact**, downloadable from the Gitea Actions run for that tag. +3. `git tag vX.Y.Z && git push origin vX.Y.Z` — this triggers the tag branch of `.gitea/workflows/build-main.yml`, which builds the frontend, runs `make appstore`, and attaches `workflow_deck_automation.tar.gz` as the asset on a normal (non-pre-) release for that tag. -**If the tagged build fails and no artifact was ever produced**, fix the issue on `main` and *move* the existing tag to the fixed commit instead of bumping to a new version number (`git tag -d vX.Y.Z && git tag -a vX.Y.Z -m "..." && git push origin :refs/tags/vX.Y.Z && git push origin vX.Y.Z`) — this is how `v0.1.0` was handled through several `build-release.yml` fixes before it first built successfully. Once a version has actually produced a published artifact, don't move its tag anymore; bump normally instead. +**If the tagged build fails and no artifact was ever produced**, fix the issue on `main` and *move* the existing tag to the fixed commit instead of bumping to a new version number (`git tag -d vX.Y.Z && git tag -a vX.Y.Z -m "..." && git push origin :refs/tags/vX.Y.Z && git push origin vX.Y.Z`) — this is how `v0.1.0` was handled through several build fixes before it first built successfully. The job deletes any pre-existing *release* for the tag before creating the new one, so a moved tag republishes cleanly instead of failing on a duplicate asset name. Once a version has actually produced a published artifact, don't move its tag anymore; bump normally instead. -**`actions/upload-artifact` must stay on `@v3`, not `@v4`+.** This self-hosted Gitea instance identifies as GHES to the official `actions/*` JS actions, which refuse to run `@actions/artifact` v2.0.0+ (used internally by `upload-artifact@v4` and later) against GHES: `GHESNotSupportedError`. `actions/checkout` and `actions/setup-node` don't have this problem and can be kept on their latest majors — only the artifact upload/download actions are affected, because they're the ones that talk to a version-specific backend API rather than just running local commands. +**No `actions/upload-artifact` anywhere — and don't reintroduce it.** This self-hosted Gitea instance identifies as GHES to the official `actions/*` JS actions, which refuse to run `@actions/artifact` v2.0.0+ (used internally by `upload-artifact@v4` and later) against GHES: `GHESNotSupportedError`, which pinned us to `@v3` for a while. Publishing through the Gitea release REST API with `curl` sidesteps that version-specific backend API entirely, so the build workflow uses that instead. `actions/checkout` and `actions/setup-node` were never affected — only the artifact upload/download actions are, because they're the ones talking to a versioned backend rather than just running local commands — and can stay on their latest majors. -### Rolling build for manual server deploys +### One workflow, two triggers -`.gitea/workflows/build-main.yml` builds the same appstore package on every push to `main` (no tag needed) and publishes it as the asset on a rolling pre-release tagged `latest-main` — the release+tag are deleted and recreated each run via the Gitea REST API (`curl`, not an `actions/*` artifact action, so the GHES artifact-API restriction above doesn't apply here), so the download URL for `workflow_deck_automation.tar.gz` on that release always serves the newest `main` build. This exists specifically so a build can be grabbed and copied onto the server (`nextcloud.gnilebein.de`) without cutting a version tag first — it produced `v0.1.0`-shaped raw-repo confusion once already when the app directory was populated by copying the git working tree instead of a built package; this gives a one-click alternative to that mistake. +`.gitea/workflows/build-main.yml` handles both cases; there is deliberately no separate release workflow, because the build steps were identical and only the publishing target differed. It runs on pushes to `main` **and** on `v*` tags, and the final step branches on `gitea.ref`: -This requires a **repo secret named `RELEASE_TOKEN`**, a Gitea Personal Access Token with repo write access (create it under the repo owner's Gitea user settings → Applications, then add it under the repo's Settings → Actions → Secrets). Without that secret, the `curl` calls to the Gitea release API in the job will fail with 401s — the workflow doesn't fall back to anything else. +- **`main`** → rolling pre-release tagged `latest-main`; both the release *and* the tag are deleted and recreated each run, so the download URL for `workflow_deck_automation.tar.gz` always serves the newest `main` build. +- **`v*`** → normal release on the tag that was just pushed. Only a stale release is deleted here — **never the tag**, since deleting it would destroy the ref that triggered the run. That's what the `RECREATE_TAG` flag guards. + +The `latest-main` half exists so a build can be grabbed and copied onto the server (`nextcloud.gnilebein.de`) without cutting a version tag first — it produced `v0.1.0`-shaped raw-repo confusion once already when the app directory was populated by copying the git working tree instead of a built package; this gives a one-click alternative to that mistake. + +Note that `make appstore` is pure `mkdir`/`tar`/`rm` — no `composer`, no `php`. The workflow therefore needs Node only; a `setup-php` step here would be dead weight (the deleted release workflow had one). + +Both halves require a **repo secret named `RELEASE_TOKEN`**, a Gitea Personal Access Token with repo write access (create it under the repo owner's Gitea user settings → Applications, then add it under the repo's Settings → Actions → Secrets). Without that secret, the `curl` calls to the Gitea release API in the job will fail with 401s — the workflow doesn't fall back to anything else. ## Data flow / architecture map @@ -68,11 +75,11 @@ This requires a **repo secret named `RELEASE_TOKEN`**, a Gitea Personal Access T ## Composer/npm lockfiles are intentionally not committed -`.gitignore` excludes `composer.lock` and `package-lock.json`. This means CI must use `composer install`/`npm install`, not `composer install --no-dev` assumptions tied to a lock file or `npm ci` (which hard-requires a lock file and will fail otherwise — this has already broken `build-release.yml` once). If you add a lockfile-dependent step, either commit the lockfile deliberately or keep using the non-`ci`/non-locked install form. +`.gitignore` excludes `composer.lock` and `package-lock.json`. This means CI must use `composer install`/`npm install`, not `composer install --no-dev` assumptions tied to a lock file or `npm ci` (which hard-requires a lock file and will fail otherwise — this has already broken the build workflow once). If you add a lockfile-dependent step, either commit the lockfile deliberately or keep using the non-`ci`/non-locked install form. ## Frontend build gotchas already hit (don't reintroduce) -None of these were discoverable locally — there's no npm here (see "Local-only notes" below), so all three were only caught by an actual Gitea Actions run against `build-release.yml`: +None of these were discoverable locally — there's no npm here (see "Local-only notes" below), so all three were only caught by an actual Gitea Actions run against the build workflow: - **`vite` version must satisfy `@nextcloud/vite-config`'s peer requirement.** `package.json` pins `@nextcloud/vite-config` to `^2.2.0`, which currently resolves to `2.5.4` and peer-requires `vite@^7.3.6`. If you bump `@nextcloud/vite-config`, check its `peerDependencies.vite` and bump our `vite` devDependency to match, or `npm install` fails with `ERESOLVE`. - **`package.json` needs `"type": "module"`.** `vite.config.js` uses `import`/`export` syntax and `@nextcloud/vite-config` is ESM-only; without `"type": "module"`, Node treats `.js` as CommonJS and `vite build` fails trying to `require()` an ESM-only package. diff --git a/README.md b/README.md index 8efde78..846d6e6 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ In [`.gitea/workflows/`](.gitea/workflows/): | `lint-php.yml` | `php -l` über eine PHP-8.2–8.4-Matrix | | `lint-info-xml.yml` | validiert `appinfo/info.xml` gegen das Appstore-XML-Schema | | `phpunit.yml` | führt die PHPUnit-Tests aus (SQLite/rein logisch, kein DB-Service nötig) | -| `build-release.yml` | baut bei Push eines `v*`-Tags das Frontend und paketiert das Appstore-Archiv | +| `build-main.yml` | baut Frontend + Appstore-Archiv und veröffentlicht es als Release-Asset — bei Push auf `main` als rollendes Pre-Release `latest-main`, bei Push eines `v*`-Tags als reguläres Release | ## Datenmodell