feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
name : Flatpak
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# One job, two outcomes:
# pull request, manual run -> build the Flatpak and run the suite inside it
# pushed tag X.Y.Z -> also release the build: publish it as the
# `stable` branch of the Catcrafts repo and
# create release X.Y.Z
#
# A release is a tag that the owner makes and signs locally and pushes with
# the master it releases; a push to master without one runs nothing. The tag
# names the version, and the metainfo's newest <release> entry has to be that
# version: its description is the release notes.
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
#
fix(ci): run on a label only the Crafter.Build runner carries
Master's last two runs (#368 and #370) failed in the Build step with
The state dir (/root/.cache/flatpak-builder) is not on the same
filesystem as the target dir (/workspace/Catcrafts/Crafter.Build)
while #369, the 2.2.0 tag run on the same commit with the same
flatpak-builder, passed. The difference was the runner. Since 14 October
the Hetzner box has a second privileged runner, cx33-org-flatpak, which
serves every Catcrafts repository under the label `flatpak` so their
package jobs can run crafter-build as a Flatpak. It mounts no host
volumes, deliberately, since any repository in the organisation can send
it jobs. Whenever it was idle first it took this job too, logged our four
volumes as "not a valid volume, will be ignored", and left the
flatpak-builder state on the container's overlay while the workspace sat
on a Docker volume. Even past that check it would have had no build
cache, no web root and no signing key.
The org runner's label is right for the jobs it was made for, and giving
it our volumes would hand the signing key to every repository in the
organisation. So the job asks for a label only its own runner carries:
`crafter-flatpak`, after the volumes and key directory that runner alone
allows. The runner on the host is relabelled to match
(/etc/forgejo-runner/config-flatpak.yaml, previous copy kept as
config-flatpak.yaml.bak-2026-10-31-label), so until this reaches master
a push there with the old `runs-on: flatpak` goes only to the org runner
and fails the same way.
AGENTS.md now says the label must stay unique and quotes the error a job
on the wrong runner stops with, so the next collision is found from the
log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:25:44 +01:00
# The `crafter-flatpak` runner label is the privileged runner on the Hetzner
# box (bubblewrap needs it) with four volumes allowed: the Sdk install, the
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
# flatpak-builder state (ccache and the LLVM module, so only crafter-build
2026-10-02 23:51:23 +02:00
# itself rebuilds on a normal run), the published repo's web root, and the
# repo signing key, which lives on the runner host and never in the forge.
fix(ci): publish under the lock Crafter.Review's workflow takes
Crafter.Review's workflow publishes its Flatpak into the same repo, the
web root's repo/, and holds an flock on .publish.lock in the web root
while it commits and updates the summary. Its comment says that this
protects both only once Crafter.Build's workflow takes the lock too, and
ours never did, so a publish of each could interleave: a
build-commit-from and a build-update-repo --generate-static-deltas
--prune of each workflow on one repo at the same time.
The Publish step now holds the same lock from its commit to the ref
files it writes into the web root, and the job installs util-linux for
flock, as Crafter.Review's jobs do.
AGENTS.template.md says that the repo is shared and the lock taken.
Neither it nor the workflow's header comment says any longer that the
web root and the signing key are this runner's alone, or that the
organisation-wide `flatpak` runner mounts none of them. That runner,
which serves every Catcrafts repository and refused all of this job's
volumes when it last took it, in October, now mounts the web root and
the key for Crafter.Review's publish job, and the volumes a runner
allows are open to every job it runs. The owner has ruled the sharing
intended. The header also stops saying that a job of ours that lands
there fails before building: that held while the runner refused every
volume, and AGENTS.template.md keeps it as the October history.
2026-11-02 12:04:06 +01:00
# No other runner carries that label. Plain `flatpak` is the label of the
# organisation's runner, which serves every Catcrafts repository and also
# mounts the web root and the key, for Crafter.Review's publish job, so any
# job it runs can mount them (AGENTS.template.md, CI and publishing).
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
on :
pull_request :
branches : [ master]
push :
2026-10-03 15:41:57 +02:00
# Bare semver tags. Spelled with globs so it means the same whether the
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# forge reads it as a workflow pattern or as a plain glob.
2026-10-03 15:41:57 +02:00
tags : [ '[0-9]*.[0-9]*.[0-9]*' ]
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
workflow_dispatch :
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# Pushes are tags only, so every release run is in one group, and a release
# starts only once the one before it has finished. A pull request's group
# also holds its number, which only a pull request's event carries, so a
# new push to it cancels its own outdated run and no other.
concurrency :
group : flatpak-${{ github.event_name }}-${{ github.event.pull_request.number }}
cancel-in-progress : ${{ github.event_name == 'pull_request' }}
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
env :
APP_ID : net.catcrafts.CrafterBuild
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
BRANCH : stable
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
MANIFEST : packaging/flatpak/net.catcrafts.CrafterBuild.yaml
CI_MANIFEST : packaging/flatpak/ci-manifest.yaml
RUNTIME : org.freedesktop.Sdk//26.08
REPO_DIR : /srv/flatpak/repo
WEB_DIR : /srv/flatpak
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# A release tag carries a signature Forgejo verifies for this address.
RELEASE_SIGNER : jorijnvdgraaf@catcrafts.net
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
jobs :
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
flatpak :
fix(ci): run on a label only the Crafter.Build runner carries
Master's last two runs (#368 and #370) failed in the Build step with
The state dir (/root/.cache/flatpak-builder) is not on the same
filesystem as the target dir (/workspace/Catcrafts/Crafter.Build)
while #369, the 2.2.0 tag run on the same commit with the same
flatpak-builder, passed. The difference was the runner. Since 14 October
the Hetzner box has a second privileged runner, cx33-org-flatpak, which
serves every Catcrafts repository under the label `flatpak` so their
package jobs can run crafter-build as a Flatpak. It mounts no host
volumes, deliberately, since any repository in the organisation can send
it jobs. Whenever it was idle first it took this job too, logged our four
volumes as "not a valid volume, will be ignored", and left the
flatpak-builder state on the container's overlay while the workspace sat
on a Docker volume. Even past that check it would have had no build
cache, no web root and no signing key.
The org runner's label is right for the jobs it was made for, and giving
it our volumes would hand the signing key to every repository in the
organisation. So the job asks for a label only its own runner carries:
`crafter-flatpak`, after the volumes and key directory that runner alone
allows. The runner on the host is relabelled to match
(/etc/forgejo-runner/config-flatpak.yaml, previous copy kept as
config-flatpak.yaml.bak-2026-10-31-label), so until this reaches master
a push there with the old `runs-on: flatpak` goes only to the org runner
and fails the same way.
AGENTS.md now says the label must stay unique and quotes the error a job
on the wrong runner stops with, so the next collision is found from the
log.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-31 01:25:44 +01:00
runs-on : crafter-flatpak
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
container :
image : archlinux:latest
options : >-
-v crafter-flatpak-system:/var/lib/flatpak
-v crafter-flatpak-builder:/root/.cache/flatpak-builder
-v /var/www/flatpak:/srv/flatpak
2026-10-02 23:51:23 +02:00
-v /etc/crafter-flatpak:/run/crafter-flatpak:ro
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
steps :
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
- name : Install tools
fix(ci): publish under the lock Crafter.Review's workflow takes
Crafter.Review's workflow publishes its Flatpak into the same repo, the
web root's repo/, and holds an flock on .publish.lock in the web root
while it commits and updates the summary. Its comment says that this
protects both only once Crafter.Build's workflow takes the lock too, and
ours never did, so a publish of each could interleave: a
build-commit-from and a build-update-repo --generate-static-deltas
--prune of each workflow on one repo at the same time.
The Publish step now holds the same lock from its commit to the ref
files it writes into the web root, and the job installs util-linux for
flock, as Crafter.Review's jobs do.
AGENTS.template.md says that the repo is shared and the lock taken.
Neither it nor the workflow's header comment says any longer that the
web root and the signing key are this runner's alone, or that the
organisation-wide `flatpak` runner mounts none of them. That runner,
which serves every Catcrafts repository and refused all of this job's
volumes when it last took it, in October, now mounts the web root and
the key for Crafter.Review's publish job, and the volumes a runner
allows are open to every job it runs. The owner has ruled the sharing
intended. The header also stops saying that a job of ours that lands
there fails before building: that held while the runner refused every
volume, and AGENTS.template.md keeps it as the October history.
2026-11-02 12:04:06 +01:00
# util-linux: flock, for the Publish step.
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
run : |
2026-04-28 23:40:27 +02:00
pacman-key --init
pacman-key --populate archlinux
pacman -Sy --noconfirm --needed archlinux-keyring
fix(ci): publish under the lock Crafter.Review's workflow takes
Crafter.Review's workflow publishes its Flatpak into the same repo, the
web root's repo/, and holds an flock on .publish.lock in the web root
while it commits and updates the summary. Its comment says that this
protects both only once Crafter.Build's workflow takes the lock too, and
ours never did, so a publish of each could interleave: a
build-commit-from and a build-update-repo --generate-static-deltas
--prune of each workflow on one repo at the same time.
The Publish step now holds the same lock from its commit to the ref
files it writes into the web root, and the job installs util-linux for
flock, as Crafter.Review's jobs do.
AGENTS.template.md says that the repo is shared and the lock taken.
Neither it nor the workflow's header comment says any longer that the
web root and the signing key are this runner's alone, or that the
organisation-wide `flatpak` runner mounts none of them. That runner,
which serves every Catcrafts repository and refused all of this job's
volumes when it last took it, in October, now mounts the web root and
the key for Crafter.Review's publish job, and the volumes a runner
allows are open to every job it runs. The owner has ruled the sharing
intended. The header also stops saying that a job of ours that lands
there fails before building: that held while the runner refused every
volume, and AGENTS.template.md keeps it as the October history.
2026-11-02 12:04:06 +01:00
pacman -Syu --noconfirm --needed flatpak flatpak-builder ccache git nodejs jq gnupg go-yq ostree python util-linux
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
git config --global --add safe.directory '*'
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
- name : Sdk
# A no-op once the volume is warm; `update` follows point releases.
run : |
flatpak remote-add --if-not-exists flathub https://dl.flathub.org/repo/flathub.flatpakrepo
flatpak install -y --noninteractive flathub "$RUNTIME"
flatpak update -y --noninteractive
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
- name : Checkout
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# Every branch and tag, for git describe and the release checks; no
# step pushes, so the token is not kept in the checkout.
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
uses : actions/checkout@v4
with :
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
fetch-depth : 0
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
persist-credentials : false
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
- name : Version
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
id : meta
run : |
set -eu
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# The metainfo's newest entry is its first <release> element, the
# one parse that gives both its version and the release notes,
# before anything is built: a comment or another attribute order
# would mislead a text search, and an entry whose notes render
# empty stops the run here. The notes go to release-notes.md, for
# the release page, as Markdown. AppStream has no headings, so a
# paragraph that introduces a list is one: <p>New</p><ul>...</ul>
# comes out as "## New" over its items, the shape the notes take
# since 3.0.0. A paragraph on its own stays a paragraph, as the
# older entries are written. <em> and <code> get their Markdown
# spelling; a code span also keeps text such as lib/<triple>/eh
# from being read as an HTML tag by the forge.
newest=$(python3 - <<'EOF'
import sys
import xml.etree.ElementTree as ET
release = ET.parse("packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml").getroot().find("releases/release")
desc = None if release is None else release.find("description")
def text(el) :
parts = [el.text or ""]
for child in el :
inner = text(child)
parts.append({"em": f"*{inner}*", "code": f"`{inner}`"}.get(child.tag, inner))
parts.append(child.tail or "")
return " ".join("".join(parts).split())
blocks = [] if desc is None else list(desc)
out = []
for i, el in enumerate(blocks) :
heads_list = i + 1 < len(blocks) and blocks[i + 1].tag in ("ul", "ol")
if el.tag == "p":
out.append(f"## {text(el)}" if heads_list else text(el))
elif el.tag == "ul":
out.append("\n".join(f"- {text(li)}" for li in el))
elif el.tag == "ol":
out.append("\n".join(f"{n}. {text(li)}" for n, li in enumerate(el, 1)))
notes = "\n\n".join(out)
if not notes.strip() :
sys.exit("the metainfo needs a <release> at the top of <releases> whose <description> renders release notes")
with open("release-notes.md", "w") as f:
print(notes, file=f)
print(release.get("version", ""))
EOF
)
if [ "$GITHUB_EVENT_NAME" = push ]; then
# A release: the only pushes this workflow runs for are tags.
# The tag is the version; it has to be annotated, signed by the
# owner as Forgejo verifies it against the keys its users added,
# on master, newer than every published release, and the version
# of the metainfo's newest entry. The tag itself is read from the
# forge: the checkout fetches a pushed tag again as a plain ref
# to its commit, so the local one is never the annotated tag.
version=$GITHUB_REF_NAME
if ! printf '%s\n' "$version" | grep -qxE '[0-9]+\.[0-9]+\.[0-9]+'; then
echo "the tag $version is not a version of the form X.Y.Z" >&2
exit 1
fi
api="$GITHUB_SERVER_URL/api/v1/repos/$GITHUB_REPOSITORY"
tag=$(curl -sSf "$api/tags/$version")
object=$(printf '%s' "$tag" | jq -r .id)
commit=$(printf '%s' "$tag" | jq -r .commit.sha)
if [ "$commit" != "$GITHUB_SHA" ]; then
echo "the forge has the tag $version on $commit, not on this run's commit $GITHUB_SHA" >&2
exit 1
fi
if [ "$object" = "$commit" ]; then
echo "$version is a lightweight tag; a release tag is annotated and signed: git tag -s $version -m 'crafter-build $version'" >&2
exit 1
fi
# Forgejo verifies a signed tag's own signature, and for an
# unsigned one falls back to its commit's, so the verdict also
# has to cover the tag's payload, not the commit's.
signed=$(curl -sSf "$api/git/tags/$object" |
jq -r --arg c "$commit" --arg v "$version" '"\(.verification.verified) \(.verification.signer.email // "") \(.verification.payload // "" | startswith("object \($c)\ntype commit\ntag \($v)\n"))"')
if [ "$signed" != "true $RELEASE_SIGNER true" ]; then
echo "Forgejo does not verify the tag $version itself as signed by $RELEASE_SIGNER (verified, signer, over the tag's payload: $signed)" >&2
exit 1
fi
if ! git merge-base --is-ancestor "$GITHUB_SHA" origin/master; then
echo "the tag $version is on $(git rev-parse --short "$GITHUB_SHA"), which is not on master: push the tag with the master it releases" >&2
exit 1
fi
# Newer than every release the forge has published, its own
# included, and not every tag: a later tag whose run still
# queues behind this one has no release yet, and a tag that is
# no X.Y.Z version never gets one.
for t in $(git tag --list | awk '/^[0-9]+\.[0-9]+\.[0-9]+$/' | sort -V); do
[ "$(printf '%s\n%s\n' " $t" "$version" | sort -V | tail -n 1)" = "$t" ] || continue
code=$(curl -s -o /dev/null -w '%{http_code}' "$api/releases/tags/$t")
case "$code" in
404 ) ;;
200 )
if [ "$t" = "$version" ]; then
echo "$version is released already; to re-run after a release left half made, delete that release first" >&2
else
echo "$version is not newer than the release $t" >&2
fi
exit 1 ;;
*) echo "the forge answered $code when asked whether $t is released" >&2; exit 1 ;;
esac
done
if [ "$newest" != "$version" ]; then
echo "the metainfo's newest release is \"$newest\", but the tag is $version: the tagged commit needs the <release> entry for $version at the top of packaging/flatpak/net.catcrafts.CrafterBuild.metainfo.xml" >&2
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
exit 1
fi
else
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# Nothing is published. The build names itself after the
# release it follows, the way a local checkout does:
# 5.0.0-3-g21746b1 is three commits past 5.0.0. Before the first
# tag there is nothing to describe from and the bare hash gets a
# placeholder in front.
described=$(git describe --tags --always)
2026-10-03 15:41:57 +02:00
case "$described" in
[ 0 -9 ] *.[0-9]*.[0-9]*) version="$described" ;;
*) version="0.0.0-dev+$described" ;;
esac
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
fi
echo "version=$version" >> "$GITHUB_OUTPUT"
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
echo "version $version"
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
- name : Build
# The manifest is rewritten beside the original so its relative
# source paths still resolve. The suite runs inside this build.
env :
VERSION : ${{ steps.meta.outputs.version }}
run : |
set -eu
2026-10-03 16:04:43 +02:00
yq '(.modules[] | select(.name == "crafter-build") | ."build-options".env.CRAFTER_BUILD_VERSION) = strenv(VERSION)' "$MANIFEST" > "$CI_MANIFEST"
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
grep -n 'CRAFTER_BUILD_VERSION' "$CI_MANIFEST"
flatpak-builder --force-clean --ccache --disable-rofiles-fuse \
--state-dir=/root/.cache/flatpak-builder \
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
--repo=repo --default-branch="$BRANCH" \
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
build-dir "$CI_MANIFEST"
- name : Smoke test
run : |
flatpak-builder --run build-dir "$CI_MANIFEST" crafter-build --version
flatpak-builder --run build-dir "$CI_MANIFEST" sh -c 'cd examples/hello-world && crafter-build && ls bin/*/hello*'
ci: ship per-march variants (v2/v3/v4) as individual artifacts
Replaces the single Linux + single Windows tarball with six self-
contained variants — three µarch levels × two OSes. End users on the
Releases page see them as six individually-downloadable archives:
crafter-build-{linux,windows}-x86_64-{v2,v3,v4}.{tar.gz,zip}
Pick the v-level your CPU supports:
v2: SSE4.2 baseline (every x86_64 since ~2011)
v3: AVX2 + FMA + BMI (Intel Haswell+, AMD Excavator+, ~2013+)
v4: AVX-512 (Skylake-X+, recent server-class)
How: the bootstrap binary stays at v2 because the CI SBC (N5105 / Tremont,
no AVX) can't execute v3 or v4 instructions and would crash before it
could rebuild itself for higher levels. Once bootstrap's done, the same
v2 binary is re-invoked with CRAFTER_BUILD_MARCH overridden per variant
to produce v2/v3/v4 outputs. Same again with --target=x86_64-w64-mingw32
for the Windows variants. ExternalDependency cache keys on (url, target,
march) already so glslang gets built per variant; subsequent CI runs hit
the cache.
Also drops the wrapper zip from workflow artifacts: instead of one
upload-artifact call producing crafter-build.zip containing six archives,
there's now one call per archive. The PR / run page shows six small
downloads named for what they are.
forgejo-release continues to upload release-dir/* as individual assets,
so the Releases page already has the right shape — this just brings the
workflow artifact UX in line.
Bumped the glslang cache key to v2 (one-time invalidation) since the
cache layout now needs space for six glslang builds rather than two.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:29:50 +02:00
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
# Everything below releases the build and runs only for a pushed tag.
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
- name : Bundle
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
if : github.event_name == 'push'
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
run : |
mkdir -p dist
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
flatpak build-bundle repo "dist/crafter-build-${{ steps.meta.outputs.version }}.flatpak" "$APP_ID" "$BRANCH"
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
ls -la dist
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
- name : Upload bundle
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
if : github.event_name == 'push'
ci: ship per-march variants (v2/v3/v4) as individual artifacts
Replaces the single Linux + single Windows tarball with six self-
contained variants — three µarch levels × two OSes. End users on the
Releases page see them as six individually-downloadable archives:
crafter-build-{linux,windows}-x86_64-{v2,v3,v4}.{tar.gz,zip}
Pick the v-level your CPU supports:
v2: SSE4.2 baseline (every x86_64 since ~2011)
v3: AVX2 + FMA + BMI (Intel Haswell+, AMD Excavator+, ~2013+)
v4: AVX-512 (Skylake-X+, recent server-class)
How: the bootstrap binary stays at v2 because the CI SBC (N5105 / Tremont,
no AVX) can't execute v3 or v4 instructions and would crash before it
could rebuild itself for higher levels. Once bootstrap's done, the same
v2 binary is re-invoked with CRAFTER_BUILD_MARCH overridden per variant
to produce v2/v3/v4 outputs. Same again with --target=x86_64-w64-mingw32
for the Windows variants. ExternalDependency cache keys on (url, target,
march) already so glslang gets built per variant; subsequent CI runs hit
the cache.
Also drops the wrapper zip from workflow artifacts: instead of one
upload-artifact call producing crafter-build.zip containing six archives,
there's now one call per archive. The PR / run page shows six small
downloads named for what they are.
forgejo-release continues to upload release-dir/* as individual assets,
so the Releases page already has the right shape — this just brings the
workflow artifact UX in line.
Bumped the glslang cache key to v2 (one-time invalidation) since the
cache layout now needs space for six glslang builds rather than two.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-29 02:29:50 +02:00
uses : actions/upload-artifact@v3
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
with :
name : crafter-build-${{ steps.meta.outputs.version }}
path : dist/*.flatpak
if-no-files-found : error
V2: WASI, -r flag, CI pipeline, examples & tests cleanup
WASI / wasm32 target support
- Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32")
- Skip -march/-mtune for wasm (clang rejects them)
- Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj
-D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync)
- .wasm output extension in expectedOutputFor and link command
- EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html +
runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL
set in the templated index.html so a single shim handles any output name
-r run flag in the CLI: build then exec the artifact (host targets only;
rejects libraries; auto .exe/.wasm extension handling)
CI pipeline (.forgejo/workflows/ci.yaml)
- Triggers: PR/push to master + manual dispatch
- Single arch-latest container job: install deps, bootstrap, self-rebuild,
run tests, cross-compile mingw, package both archives, upload artifacts
- Rolling 'latest' release published only on push/dispatch to master
mingw cross-compile from Linux now works end-to-end:
- ExternalDependency cache key includes target so per-target glslang builds
don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends
'd' to lib names and breaks linking); cross-compile cmake flags
(CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...)
- project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic
and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh
link pulls pthread which mingw doesn't link by default)
- mingw compile uses -femulated-tls so std::__once_callable etc reference
the same emutls symbols libstdc++ provides
- mingw link auto-adds -lstdc++exp -lpthread
GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux
+ Windows) now both use it instead of duplicating the resolution.
Examples reorg: hello-world, library, with-module, wasi, tests — each with
its own README. Tests reorg: per-test directory with inner/ fixture, no
shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
- name : Publish to the Catcrafts repo
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
if : github.event_name == 'push'
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
run : |
set -eu
2026-10-02 23:51:23 +02:00
# The signing key is generated once on the runner host
# (/etc/crafter-flatpak: signing-key.asc and key-id) and mounted
# read-only; it is imported into a throwaway keyring per run.
if [ ! -s /run/crafter-flatpak/signing-key.asc ] || [ ! -s /run/crafter-flatpak/key-id ]; then
2026-10-31 16:56:11 +01:00
echo "no signing key under /etc/crafter-flatpak on the runner host; generate it as described in AGENTS.template.md (CI and publishing)" >&2
2026-10-02 23:51:23 +02:00
exit 1
fi
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
export GNUPGHOME=$(mktemp -d)
2026-10-02 23:51:23 +02:00
gpg --batch --quiet --import /run/crafter-flatpak/signing-key.asc
FLATPAK_GPG_KEY_ID=$(cat /run/crafter-flatpak/key-id)
fix(ci): publish under the lock Crafter.Review's workflow takes
Crafter.Review's workflow publishes its Flatpak into the same repo, the
web root's repo/, and holds an flock on .publish.lock in the web root
while it commits and updates the summary. Its comment says that this
protects both only once Crafter.Build's workflow takes the lock too, and
ours never did, so a publish of each could interleave: a
build-commit-from and a build-update-repo --generate-static-deltas
--prune of each workflow on one repo at the same time.
The Publish step now holds the same lock from its commit to the ref
files it writes into the web root, and the job installs util-linux for
flock, as Crafter.Review's jobs do.
AGENTS.template.md says that the repo is shared and the lock taken.
Neither it nor the workflow's header comment says any longer that the
web root and the signing key are this runner's alone, or that the
organisation-wide `flatpak` runner mounts none of them. That runner,
which serves every Catcrafts repository and refused all of this job's
volumes when it last took it, in October, now mounts the web root and
the key for Crafter.Review's publish job, and the volumes a runner
allows are open to every job it runs. The owner has ruled the sharing
intended. The header also stops saying that a job of ours that lands
there fails before building: that held while the runner refused every
volume, and AGENTS.template.md keeps it as the October history.
2026-11-02 12:04:06 +01:00
# Crafter.Review's workflow publishes into the same repo and holds
# this lock while it does, so a commit and the summary update of
# one workflow never interleave with the other's.
exec 9> "$WEB_DIR/.publish.lock"
flock 9
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
# The live repo is the canonical copy; builds are promoted into it,
# never built into it.
2026-10-03 14:40:09 +02:00
# Keyed on the repo's config file, not the directory: the web root
# may exist as an empty directory before the first publish.
[ -f "$REPO_DIR/config" ] || ostree init --repo="$REPO_DIR" --mode=archive
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
flatpak build-commit-from --src-repo=repo \
--gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
"$REPO_DIR" "app/$APP_ID/x86_64/$BRANCH"
# Every push to master used to be published as `beta`. Its last
# build is marked end-of-life, once, so that the next
# `flatpak update` of a beta installation says why nothing follows.
# The reason names the branch: with --noninteractive, flatpak's
# message names only the app.
beta="app/$APP_ID/x86_64/beta"
if ostree rev-parse --repo="$REPO_DIR" "$beta" >/dev/null 2>&1 &&
! ostree show --repo="$REPO_DIR" --print-metadata-key=ostree.endoflife "$beta" >/dev/null 2>&1; then
flatpak build-commit-from --src-ref="$beta" \
--end-of-life="The beta branch gets no more updates: every change now comes as a release on the stable branch. Install stable with flatpak install catcrafts net.catcrafts.CrafterBuild//stable, then remove beta with flatpak uninstall net.catcrafts.CrafterBuild//beta." \
--gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \
"$REPO_DIR" "$beta"
fi
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
flatpak build-update-repo --gpg-sign="$FLATPAK_GPG_KEY_ID" --gpg-homedir="$GNUPGHOME" \
--generate-static-deltas --prune "$REPO_DIR"
# The repo description and the app ref carry the public key.
key=$(gpg --export "$FLATPAK_GPG_KEY_ID" | base64 --wrap=0)
sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/catcrafts.flatpakrepo > "$WEB_DIR/catcrafts.flatpakrepo"
sed "s|GPGKey=.*|GPGKey=$key|" packaging/flatpak/net.catcrafts.CrafterBuild.flatpakref > "$WEB_DIR/net.catcrafts.CrafterBuild.flatpakref"
fix(ci): publish under the lock Crafter.Review's workflow takes
Crafter.Review's workflow publishes its Flatpak into the same repo, the
web root's repo/, and holds an flock on .publish.lock in the web root
while it commits and updates the summary. Its comment says that this
protects both only once Crafter.Build's workflow takes the lock too, and
ours never did, so a publish of each could interleave: a
build-commit-from and a build-update-repo --generate-static-deltas
--prune of each workflow on one repo at the same time.
The Publish step now holds the same lock from its commit to the ref
files it writes into the web root, and the job installs util-linux for
flock, as Crafter.Review's jobs do.
AGENTS.template.md says that the repo is shared and the lock taken.
Neither it nor the workflow's header comment says any longer that the
web root and the signing key are this runner's alone, or that the
organisation-wide `flatpak` runner mounts none of them. That runner,
which serves every Catcrafts repository and refused all of this job's
volumes when it last took it, in October, now mounts the web root and
the key for Crafter.Review's publish job, and the volumes a runner
allows are open to every job it runs. The owner has ruled the sharing
intended. The header also stops saying that a job of ours that lands
there fails before building: that held while the runner refused every
volume, and AGENTS.template.md keeps it as the October history.
2026-11-02 12:04:06 +01:00
flock -u 9
2026-11-02 11:39:01 +01:00
# The release attaches this .flatpakref, not the one in the
# checkout: that one has a placeholder where the key goes, and
# flatpak cannot install from it.
cp "$WEB_DIR/net.catcrafts.CrafterBuild.flatpakref" dist/
(cd dist && sha256sum * > SHA256SUMS)
ls -la dist
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
- name : Publish release
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
if : github.event_name == 'push'
# The notes are release-notes.md, which the Version step wrote. No
# `override`: it would delete a tag that has moved off this commit
# since the Version step and tag the old commit instead. Without it
# the action refuses such a tag, and a release that an earlier
# attempt left half made, such as a draft whose upload failed, has
# to be deleted before a re-run.
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
uses : https://code.forgejo.org/actions/forgejo-release@v2
with :
direction : upload
url : ${{ github.server_url }}
repo : ${{ github.repository }}
feat(ci): drop beta and latest, release signed tags on master
A push to master published the `beta` branch and moved the rolling
`latest` tag and its "Latest master build" prerelease, and a pushed
X.Y.Z tag made the release and published `stable`. The owner wants a
release for every push to master, made by the signed tag that goes with
it, and nothing for a push without one. So `beta`, `latest` and the
branch trigger are gone: the workflow runs for pushed tags, pull
requests and manual runs only, and the `latest` tag and its prerelease
have been deleted from the forge.
`beta` gets no more builds, and its installations would not hear of it:
the release notes ship in stable builds only. The Publish step marks
beta's last build end-of-life, once, with a reason that names stable,
and the next `flatpak update` of a beta installation prints it. The
reason also names the branch, because a non-interactive update prints
only the app's name with it.
Before building, a tag run now checks the tag: it has to be annotated
and signed by the owner as Forgejo verifies it, its commit has to be on
master, its version has to be X.Y.Z and newer than every release the
forge has published, its own included (a later tag whose run still
queues does not count), and the metainfo's newest <release> entry has to
be that version. The tag run checked only the last of these. It reads
the tag from the forge's API, because the checkout fetches a pushed tag
again as a plain ref to its commit, so the job's clone never has the
annotated tag, and Forgejo verifies a signed tag's own signature but
falls back to the commit's for an unsigned one, so the step also
requires that the verdict covers the tag's payload. The metainfo's entry
is read once, as the first <release> element: the Version step takes its
version and renders its notes into release-notes.md with the renderer
the Release notes step had, and stops the run when they come out empty,
so a comment or another attribute order cannot give a release one
entry's version and another's notes. It does so on every run, pull
requests and manual runs included, so a broken entry shows before a
release.
Release runs share a concurrency group, so they queue one behind
another; a pull request still cancels its own outdated run. A failed
release run can be re-run while no newer release exists, since its tag
is still on its commit; a release that an attempt left half made has to
be deleted first. The release step runs without `override`, which would
delete a tag that has moved off the run's commit since the Version step
and tag the old commit instead.
A manual run no longer publishes anything; it used to publish `beta`
and move `latest` from whichever branch it was started on. No step
pushes to git, so the checkout keeps no credentials. git describe in
build.sh, project.cpp and the workflow has no `latest` tag left to
exclude.
2026-11-02 11:42:58 +01:00
tag : ${{ steps.meta.outputs.version }}
title : crafter-build ${{ steps.meta.outputs.version }}
2026-10-03 15:41:57 +02:00
release-notes-file : release-notes.md
feat(ci): build and publish the Flatpak
One job replaces the tarball pipeline. On a pull request it builds the
Flatpak with flatpak-builder, which runs the bootstrap, the self-host
build and the test suite inside the sandbox, and smoke-tests the result
with `crafter-build --version` and the hello-world example. On a push to
master it also bundles the build, promotes it into the live repo as the
`beta` branch and refreshes the rolling `latest` prerelease. On a tag
X.Y.Z it promotes as `stable`, refuses to proceed unless the metainfo's
newest release is that version, and publishes a release whose notes are
that entry's description.
The job runs on the `flatpak` runner label: the privileged forgejo-runner
on the Hetzner box, with the Sdk install, the flatpak-builder state and
the repo's web root as volumes. The state volume is what keeps a normal
run short: flatpak-builder rebuilds the LLVM and WASI modules only when
their sources change. The live repo is canonical and never built into;
builds are promoted with build-commit-from, then build-update-repo signs
and prunes it and generates static deltas.
The musl job, the per-march tarballs, the toolchain packages the runner
used to install and the PKGBUILD go: the Flatpak is the one artifact.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-02 22:36:55 +02:00
release-dir : dist
token : ${{ secrets.GITHUB_TOKEN }}