Deployment Repository Publishing
Publishing commits a generated remote-ansible package to a Git deployment repository, where a GitHub Actions
workflow deploys it. This page explains what the review page sends, which repositories the server accepts, how a
publish works, and how the deployment repository template consumes the package. Read it when you work on the publish
action or set it up for a test server.
What publishing does
On the review page, the Remote server (Ansible) target shows a Target name field. When publishing is
configured, it also shows a Deployment repository field and a Deploy to <target> button. Both field
values are remembered in the browser. The button sends the selection and the repository URL to
POST /api/feature-model/deployment-package/publish. The server generates the package, commits it to
<target-directory-root>/<target>/package/ on the configured branch of that repository, and answers with the commit.
The client then downloads the same package as a ZIP file.
The publish and the download use the same generation request, and generation is deterministic, so the published files and the downloaded ZIP file are identical. When the repository refuses the publish, the client still downloads the package and shows the error. When the selection itself cannot be packaged, for example because it uses MySQL, the client shows one error card and downloads nothing.
GET /api/feature-model/deployment-package/publish-target tells the client whether publishing is configured, which
default repository URL to prefill, and the branch and directory a publish uses. Without a usable configuration, the
repository field and the button do not appear.
The target name is an exact identifier: 1 to 63 lowercase letters, digits, or hyphens, starting with a letter or a
digit, for example test-vm. It names the package directory and the GitHub Environment the deployment runs in, so
the server checks it instead of changing it. The field shows the Environment name and blocks both buttons while the
name is invalid.
Configuration
Publishing is configured under artemis.feature-model.deployment-repository. The committed application.yml switches
it on and uses the project's deployment repository as the default URL.
| Property | Meaning |
|---|---|
enabled | Switches publishing on |
repository-url | Default repository URL: prefilled in the field and used when a request has none; HTTPS, SSH, or file://, without credentials |
allowed-repository-url-prefixes | Prefixes every repository URL must start with; defaults to https://github.com/, git@github.com:, and ssh://git@github.com/ |
branch | The branch that receives the commits; defaults to main |
target-directory-root | The directory that holds one folder per target; defaults to deployments |
expected-visibility | Optional public or private for a GitHub repository; the server refuses to push when it differs |
author-name, author-email | The author of the commits |
Credentials are never configuration properties. The server reads them only from environment variables and never logs or returns them:
| Remote | Environment variables |
|---|---|
| HTTPS | FM_DEPLOYMENT_REPO_TOKEN, an access token |
| SSH | FM_DEPLOYMENT_REPO_SSH_KEY_PATH, the path of a deploy key, and FM_DEPLOYMENT_REPO_KNOWN_HOSTS_PATH, the path of a known-hosts file |
SSH uses strict host key checking with exactly this known-hosts file. file:// remotes need no credentials. The
review page offers publishing when it is enabled and at least one transport is usable: the token is set, both SSH
files are readable, or a file:// prefix is allowed.
Which repositories the server accepts
A repository URL comes from the browser, and the server clones and pushes it with its own credentials. Two checks keep these credentials on the repositories they are meant for.
The URL policy runs before any Git operation, also for the default URL. The server trims the URL and refuses it when
it contains whitespace, ?, #, or a .. segment, carries user information such as https://user@github.com/...,
or does not start with an allowed prefix followed by a repository path. A file:// URL is accepted only when an
operator adds a file:// prefix, which is what tests and local try-outs do.
The repository must also declare itself a deployment repository. The server reads deployment-repository.json at the
root of the cloned branch before it changes anything:
{ "kind": "artemis-feature-model-deployment-repository", "packageContract": 1 }
packageContract must equal the major version of RuntimePackageConstants.REMOTE_ANSIBLE_PACKAGE_VERSION. A missing
or unreadable file, another kind, or another contract refuses the publish, and nothing is committed or pushed.
How a publish works
- The server resolves the repository URL from the request or the default, applies the URL policy, and checks the credential of the URL's transport.
- It clones the configured branch into a temporary directory, shallow and with only that branch. When the branch does not exist yet, it clones the default branch and creates the branch from it.
- It checks the marker file.
- It replaces the target's
packagedirectory completely with the generated files. - When nothing changed, it stops and reports the existing commit as up to date.
- Otherwise it commits and pushes. It never force-pushes, so no existing commit can be lost.
- When the push fails because someone else pushed first, it starts again from a fresh clone, up to three attempts.
When expected-visibility is set, the server asks the GitHub API for the visibility of each repository once after
startup, before the first push to it.
The deployment repository template
Administrators create a deployment repository from the public template ls1intum/artemis-feature-model-deployment-template. Its README is the administrator guide. The template contains the marker file, a pinned Ansible toolchain, and two workflows:
- Deploy package runs for every push to
deploymentthat changes a package, and on demand. It finds the single changed target, checks the package against the marker, and deploys inside the GitHub Environment named after the target. Before it connects to the VM, it lists every missing connection value and package value in the run summary. - Check connection tests the SSH access to a target's VM and reports its host keys.
The workflow reads the package through a fixed set of paths: playbook.yml, requirements.yml, preflight.sh,
inventory/hosts, inventory/group_vars/artemistests_without_*.yml, metadata/package-manifest.json, and
metadata/env-references.json. Each entry of env-references.json carries the variable's kind and an
administrator-facing description from the binding catalog, and the package README shows the same list as a table.
A change to any of these paths or fields is a change of the package contract: raise the major version of
REMOTE_ANSIBLE_PACKAGE_VERSION and the template's packageContract together.
GitHub holds workflows that read all secrets at once, so the template's workflow names every secret a package can
reference. When the binding catalog gains a secret, add it to the template's deploy.yml as well; older copies then
stop before deploying and ask their administrators to update the workflow.
Errors
Publishing errors use codes that start with PUBLISH_:
| Code | Status | Meaning |
|---|---|---|
PUBLISH_NOT_CONFIGURED | 409 | Publishing is off, or the credential of the URL's transport is missing |
PUBLISH_REQUIRES_REPOSITORY_URL | 400 | Neither the request nor the configuration names a repository |
PUBLISH_REPOSITORY_NOT_ALLOWED | 400 | The URL violates the URL policy; the message lists the allowed prefixes |
PUBLISH_WRONG_DEPLOYMENT_MODE | 400 | The request is not for remote-ansible |
PUBLISH_REQUIRES_TARGET_NAME | 400 | The request has no target name |
PUBLISH_INVALID_TARGET_NAME | 400 | The target name is not an exact identifier |
PUBLISH_NOT_A_DEPLOYMENT_REPOSITORY | 409 | The marker file is missing or declares another kind or contract |
PUBLISH_REJECTED | 409 | The repository rejected the push |
PUBLISH_AUTH_FAILED | 502 | The repository refused the credential |
PUBLISH_VISIBILITY_MISMATCH | 409 | The repository's visibility differs from expected-visibility |
An invalid or unsupported selection fails with the same ARTIFACT_GENERATION_* errors as the download.
Try it locally
You can publish to a local bare repository. Create one whose main branch holds the marker file:
git init --bare -b main /tmp/deployments.git
git clone /tmp/deployments.git /tmp/deployments-seed
printf '{ "kind": "artemis-feature-model-deployment-repository", "packageContract": 1 }\n' > /tmp/deployments-seed/deployment-repository.json
git -C /tmp/deployments-seed add deployment-repository.json
git -C /tmp/deployments-seed commit -m "add the deployment repository marker"
git -C /tmp/deployments-seed push origin main
Then start the server with this repository as the default and its directory as an allowed prefix:
./gradlew bootRun --args='--artemis.feature-model.deployment-repository.repository-url=file:///tmp/deployments.git --artemis.feature-model.deployment-repository.allowed-repository-url-prefixes=file:///tmp/'
In the Configurator, switch the database to PostgreSQL in the advanced tree, open the review page, choose Remote
server (Ansible), enter a target name, and click Deploy to <target>.
git --git-dir=/tmp/deployments.git log deployment shows the new commit.
Where it lives in the code
| Path | Responsibility |
|---|---|
export/web/DeploymentPackageResource | The publish and publish-target endpoints |
export/dto/DeploymentPackagePublishRequest | The publish request: generation request plus repository URL |
export/service/DeploymentPackagePublishService | Generates the package and starts the publish |
export/service/DeploymentRepositoryPublisher | Target name, marker check, clone, commit, push, and retries |
export/service/DeploymentRepositoryUrlPolicy | The URL policy |
export/service/DeploymentRepositorySshTransport | SSH key and host key handling |
export/service/DeploymentRepositoryProperties | The configuration properties |
shared/exception/DeploymentRepositoryPublishException | The PUBLISH_* errors |
Paths are relative to src/main/java/de/tum/cit/aet/artemis/featuremodel/.