Gate the release on the lint and test jobs
Build package / php-lint (8.2) (push) Successful in 47s
Build package / php-lint (8.3) (push) Successful in 36s
Build package / php-lint (8.4) (push) Successful in 44s
Build package / xml-lint (push) Successful in 14s
Build package / unit-tests (push) Successful in 40s
Build package / package (push) Successful in 1m1s

The four workflows all triggered on the same push to main and ran fully
independently, so build-main.yml published a latest-main release even when
PHPUnit or the linters were red. `needs:` only works between jobs of one
workflow, so the checks move into build-main.yml and the package job now
depends on them.

Two gaps close along the way: the check workflows only ran on main pushes
and pull requests, so a v* tag was published entirely unverified, and
`npm run build` only ran on main, so no pull request ever exercised the
frontend build -- the workflow now runs on pull_request too, with the
publish step skipped so the job acts as a build check.

Build and publish deliberately stay in a single job; moving the tarball
between jobs would require actions/upload-artifact (unusable on this Gitea,
see CLAUDE.md) or actions/cache.
This commit is contained in:
Patrick Niebeling
2026-08-13 14:17:00 +02:00
parent 85b42d84c9
commit 8f3e3e694a
5 changed files with 73 additions and 76 deletions
+60
View File
@@ -1,14 +1,73 @@
name: Build package name: Build package
# One workflow for checks, build and release, because the publish step has to
# depend on the checks and `needs:` only works between jobs of one workflow.
on: on:
push: push:
branches: branches:
- main - main
tags: tags:
- 'v*' - 'v*'
pull_request:
jobs: jobs:
php-lint:
runs-on: gitea-runner-server03
strategy:
matrix:
php-version: ['8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: none
- name: Syntax check
run: |
find lib tests appinfo -name '*.php' -print0 | xargs -0 -n1 -- php -l
xml-lint:
runs-on: gitea-runner-server03
steps:
- uses: actions/checkout@v7
- name: Install xmllint
run: |
apt-get update
apt-get install -y --no-install-recommends libxml2-utils
- name: Download appstore schema
run: wget -q https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd -O info.xsd
- name: Validate appinfo/info.xml
run: xmllint --schema info.xsd appinfo/info.xml --noout
unit-tests:
runs-on: gitea-runner-server03
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: sqlite, pdo_sqlite
coverage: none
- name: Install dependencies
run: composer install --no-progress --prefer-dist
- name: Run unit tests
run: composer run test:unit
# Building and publishing stay in one job on purpose: handing the tarball to a
# separate publish job would mean actions/upload-artifact (unusable here, see
# CLAUDE.md) or actions/cache, and neither is worth a second Gitea backend API
# in the critical path. On a pull request this job stops after `make appstore`
# and just acts as a build check.
package: package:
needs: [php-lint, xml-lint, unit-tests]
runs-on: gitea-runner-server03 runs-on: gitea-runner-server03
# contents: write maps to Code: write (deleting the latest-main tag) plus # contents: write maps to Code: write (deleting the latest-main tag) plus
# Releases: write (creating releases and uploading the asset) - everything # Releases: write (creating releases and uploading the asset) - everything
@@ -32,6 +91,7 @@ jobs:
run: make appstore run: make appstore
- name: Publish release - name: Publish release
if: gitea.event_name == 'push'
env: env:
GITEA_API: ${{ gitea.server_url }}/api/v1 GITEA_API: ${{ gitea.server_url }}/api/v1
REPO: ${{ gitea.repository }} REPO: ${{ gitea.repository }}
-23
View File
@@ -1,23 +0,0 @@
name: Lint info.xml
on:
push:
branches: [main]
pull_request:
jobs:
xml-lint:
runs-on: gitea-runner-server03
steps:
- uses: actions/checkout@v7
- name: Install xmllint
run: |
apt-get update
apt-get install -y --no-install-recommends libxml2-utils
- name: Download appstore schema
run: wget -q https://raw.githubusercontent.com/nextcloud/appstore/master/nextcloudappstore/api/v1/release/info.xsd -O info.xsd
- name: Validate appinfo/info.xml
run: xmllint --schema info.xsd appinfo/info.xml --noout
-24
View File
@@ -1,24 +0,0 @@
name: Lint PHP
on:
push:
branches: [main]
pull_request:
jobs:
php-lint:
runs-on: gitea-runner-server03
strategy:
matrix:
php-version: ['8.2', '8.3', '8.4']
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
coverage: none
- name: Syntax check
run: |
find lib tests appinfo -name '*.php' -print0 | xargs -0 -n1 -- php -l
-24
View File
@@ -1,24 +0,0 @@
name: PHPUnit
on:
push:
branches: [main]
pull_request:
jobs:
unit-tests:
runs-on: gitea-runner-server03
steps:
- uses: actions/checkout@v7
- uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: sqlite, pdo_sqlite
coverage: none
- name: Install dependencies
run: composer install --no-progress --prefer-dist
- name: Run unit tests
run: composer run test:unit
+13 -5
View File
@@ -46,23 +46,31 @@ There is no meaningful way to test the Deck integration itself outside a real Ne
### Release process ### Release process
1. Bump `<version>` in `appinfo/info.xml` and `version` in `package.json` (keep them in sync). 1. Bump `<version>` 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. 2. Commit, push to `main`, confirm the run of `.gitea/workflows/build-main.yml` is green.
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. 3. `git tag vX.Y.Z && git push origin vX.Y.Z` — this triggers the tag branch of the same workflow, which re-runs the checks, 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 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. **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.
**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. **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.
### One workflow, two triggers ### One workflow for everything
`.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`: `.gitea/workflows/build-main.yml` is the *only* workflow: it carries the checks (`php-lint` across 8.2/8.3/8.4, `xml-lint`, `unit-tests`) and the `package` job that builds and publishes. They used to be four separate workflow files, which meant they all triggered independently on the same push and a `latest-main` release was published even when PHPUnit was red. `needs:` only works between jobs of the same workflow (and Gitea's `workflow_run` can depend on one workflow at a time), so gating the release on the checks required merging them into this file. Don't split them back out.
Consequences of that shape, all intentional:
- **`package` builds *and* publishes in one job.** Handing the tarball to a separate publish job would need `actions/upload-artifact` (unusable here, see above) or `actions/cache` — a second Gitea backend API in the critical path for no gain.
- **The workflow also runs on `pull_request`**, where the `Publish release` step is skipped via `if: gitea.event_name == 'push'` and the job degrades into a frontend-build check. That matters: `npm run build` used to run only on `main`, so no PR ever exercised it.
- **Tags now get linted and tested too.** Previously the check workflows triggered only on `main` pushes and PRs, so a `v*` tag published completely unverified.
It runs on pushes to `main` **and** on `v*` tags, and the publish step branches on `gitea.ref`:
- **`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. - **`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. - **`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. 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). Note that `make appstore` is pure `mkdir`/`tar`/`rm` — no `composer`, no `php`. The `package` job therefore needs Node only; a `setup-php` step *there* would be dead weight (the deleted release workflow had one). PHP is set up in the check jobs, which is a different job and a different container.
**No manually created token is needed.** The publish step authenticates with `secrets.GITEA_TOKEN`, the token Gitea injects into every Actions job automatically (Gitea 1.27.1 here). The job declares `permissions: contents: write`, which Gitea maps to *Code: write* (needed to delete the `latest-main` tag) plus *Releases: write* (creating the release and uploading the asset). **No manually created token is needed.** The publish step authenticates with `secrets.GITEA_TOKEN`, the token Gitea injects into every Actions job automatically (Gitea 1.27.1 here). The job declares `permissions: contents: write`, which Gitea maps to *Code: write* (needed to delete the `latest-main` tag) plus *Releases: write* (creating the release and uploading the asset).