Releases¶
Releases follow the repository blueprint: nobody chooses a version or writes release notes by hand.
---
config:
flowchart:
rankSpacing: 30
---
flowchart TB
accTitle: From pull requests to a published release
accDescr: Pull requests titled fix, feat or feat! are merged into main, and the release bot keeps the pull request chore: release x.y.z with the version and the changelog. Once the maintainer merges it, a draft release with its tag is created, the package is built and checked, signed with the app's certificate, and gets its signature, SBOM and provenance. Then the release is published and immutable, delivered to the Nextcloud App Store, and verified, and every week again.
subgraph request ["The release pull request"]
direction TB
prs["Pull requests<br>fix: · feat: · feat!:"] -- "merged<br>into main" --> bot["chore: release x.y.z<br>version and changelog"]
end
subgraph build ["Once the maintainer merges it"]
direction TB
draft["Draft release<br>with its tag"] --> package["Package built<br>and checked"] --> sign["Signed with the<br>app's certificate"] --> assets["Signature, SBOM,<br>provenance"]
end
subgraph publication ["Publication"]
direction TB
published["Release published,<br>immutable"] --> store["Nextcloud<br>App Store"] --> verify["Release verified,<br>and every week again"]
end
request --> build --> publication
classDef person fill:#f59e0b2e,stroke:#f59e0b,stroke-width:2px
classDef bot fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
class prs person
class bot,draft,package,sign,assets bot
class published,store,verify done
The release pull request¶
Every pull request that changes something for users describes it under ## Unreleased in CHANGELOG.md. The release bot (.github/workflows/release.yml) keeps a pull request titled chore: release x.y.z up to date with main:
- The version follows from the titles of the pull requests merged since the last release:
fixmakes a patch,feata minor and!orBREAKING CHANGEa major version. Every merge decides it anew. A lineRelease-As: x.y.zin the description of a pull request sets it explicitly. - The changelog: the text of Unreleased becomes the section of the release; without one, the section lists the pull requests.
- The files of the version:
appinfo/info.xml(<version>and the screenshot URLs markedx-release-please-version),version.txtand.release-please-manifest.json.
The pull request needs the same required checks as every other one. Its Docker end-to-end tests pass at once, as a release changes nothing of the app but its version: they ran against every supported Nextcloud version in the pull requests of its changes and with every push to main (decision 0005). A release of dependency updates alone merges itself; every other release waits for the maintainer to merge it.
Publication¶
Merging the release pull request creates the release as a draft, with its tag, and then:
- krankerl builds the package of the tagged commit;
scripts/check-package.shchecks that it holds exactly the runtime files of that commit. - The package is signed with the app's certificate (
occ integrity:sign-app) and checked again, and a detached SHA-512 signature is made and verified with the public key of the certificate. - The release gets the package
paperless_sync.tar.gz, its signaturepaperless_sync.tar.gz.sig, an SPDX SBOM, and the signed build provenance of every asset (provenance.sigstore.jsonandprovenance.intoto.jsonl). - The complete release is published; GitHub keeps it immutable from then on.
- The same package is submitted to the Nextcloud App Store.
- The release verification (
.github/workflows/verify-release.yml) checks the release as its users can: every attestation, the SBOM, the immutability, the signed commit, and the version in the App Store. It checks the latest release every week as well.
What the package holds¶
The package holds what Nextcloud loads of the app, appinfo/, css/, img/, js/, l10n/, lib/ and templates/, and the CHANGELOG.md, LICENSE and README.md that the App Store and administrators read; the tests, the documentation and the tools of the repository stay out. krankerl decides what it leaves out by .nextcloudignore, so scripts/check-package.sh doesn't trust that list: it compares the package, file by file, with the runtime files that Git tracks in the commit that krankerl packaged.
---
config:
flowchart:
nodeSpacing: 24
---
flowchart TB
accTitle: What the check of the package compares
accDescr: krankerl packages the commit in HEAD and leaves out what .nextcloudignore lists. scripts/check-package.sh takes the runtime files of the same commit, those in appinfo, css, img, js, l10n, lib and templates and CHANGELOG.md, LICENSE and README.md, and compares the package with them file by file. A file that the package lacks, a stray file, a changed file or a link fails the check with the list of the differences. A package that holds exactly these files, and its signature once it is signed, passes if its PHP and JavaScript parse, and otherwise fails with the file that doesn't. The release workflow checks the package again after signing.
head["The commit<br>in HEAD"]
krankerl["`krankerl packages it<br>*and leaves out*<br>*what it ignores*`"]
package["The package"]
runtime["Its runtime files:<br>appinfo · css · img<br>js · l10n · lib<br>templates<br>changelog, license<br>and README"]
same{"The same files,<br>each with the same content?"}
differences["❌ Fails with the list:<br>missing · stray · changed"]
parse{"Does its PHP and<br>JavaScript parse?"}
broken["❌ Fails with the file<br>that doesn't parse"]
passed["✅ Passes, and again<br>after signing"]
head --> krankerl --> package --> same
head --> runtime --> same
same -- "no" --> differences
same -- "yes" --> parse
parse -- "no" --> broken
parse -- "yes" --> passed
classDef source fill:#17803d2e,stroke:#17803d,stroke-width:2px
classDef check fill:#526cfe2e,stroke:#526cfe,stroke-width:2px
classDef done fill:#16a34a2e,stroke:#16a34a,stroke-width:2px
classDef refused fill:#c9302c1f,stroke:#c9302c,stroke-width:2px
class head,runtime source
class krankerl,package,same,parse check
class passed done
class differences,broken refused
A package fails the check, with one line for each difference, when it
- lacks a runtime file (
missing:), such as the settings page intemplates/, the German translation or a migration of the database, which a change of.nextcloudignoreor of krankerl could drop; - holds a stray file (
stray:), one that Git doesn't track in a runtime path or that belongs to the development, such ascomposer.jsonortests/; - holds a changed file (
changed:), whose content differs from the commit, such as one with the line endings of Windows; - holds a link, a device or an entry whose path leads out of the folder of the app;
- lacks its signature after signing, or holds one before it;
- or holds PHP or JavaScript that doesn't parse.
The CI runs the same check on the package of every pull request, so a change that breaks the package fails there and not in a release. A new folder that the app needs at runtime belongs in RUNTIME_PATHS of scripts/check-package.sh, and .nextcloudignore must not list it; a path that the app no longer has leaves RUNTIME_PATHS, and the check names it until then. tests/package/test_check_package.sh tests the check with packages that it must reject.
Secrets and recovery¶
The release environment allows only main. It holds APP_PRIVATE_KEY (the key of the app's certificate), APPSTORE_TOKEN and RELEASE_AUTOMATION_PRIVATE_KEY (the release app, whose client ID is the repository variable RELEASE_AUTOMATION_CLIENT_ID). Every token is short-lived and scoped to what its job needs.
If a job fails, re-run the failed jobs of the Release workflow: a draft is completed, and the App Store accepts the same version again without a second release. A published release is never replaced.
Registering the app in the App Store, needed once and again only for a new certificate, is the manual workflow Register app in Nextcloud App Store.