Publishing to repo.protequ.com¶
repo.protequ.com is the Protequ Forgejo instance. It hosts the container registry every PQ
installation pulls from, and the NuGet feed your adapter builds against.
You publish exactly the way Protequ's own adapters are published: a tag push in your repository
triggers a workflow that logs into the registry and pushes a semver-tagged image into the pq
namespace.
Credentials¶
Protequ issues you a Forgejo account with a personal access token. The same account covers:
| Use | Endpoint |
|---|---|
| Container registry (push) | repo.protequ.com |
| NuGet feed (read) | https://repo.protequ.com/api/packages/PQ/nuget/index.json |
In GitHub, store them as repository secrets — PQ_REGISTRY_USERNAME and PQ_REGISTRY_TOKEN.
Never commit them, and never bake them into the final image stage.
Image naming¶
repo.protequ.com/pq/<image>:<version>
│ │ └── semver, e.g. 1.0.0 or 1.0.0-rc.1
│ └── your image name: lowercase, "pq" prefix; hyphens are allowed
└── the namespace every PQ install scans
Only images under pq/ are discovered — the installer and the public catalog both filter on
that namespace.
The installer derives several things from <image>, which is why the name matters and must
never change between releases:
From image pqacme |
Derived |
|---|---|
| Container name | adapter-acme (the pq prefix is stripped) |
| Compose profile | pqacme |
| Tag override variable | PQACME_TAG — lets an operator pin one adapter to a specific version |
| nginx route (UI adapters) | /adapters/<ui.slug>/, defaulting to acme |
If the image name contains a hyphen, only the tag override variable uses an underscore:
pq-tools becomes PQ_TOOLS_TAG. The registry image, Compose profile, and container identity keep
the original image name.
Tagging rules¶
- The image tag is the version. Valid semver only:
^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ - A prerelease is any version containing a
-(1.0.0-rc.1,1.2.0-beta.2). - A prerelease must never move
latest.latestis the stable channel that fresh installs land on; publishing an RC to it ships unreleased code to every new customer. Push a prerelease under its version tag only. - A stable release pushes two tags:
1.0.0andlatest. - Never re-push a version tag with different content. Bump the version instead — installations pin digests and will not notice a silent replacement.
GitHub Actions example¶
A complete, minimal release workflow. Push the tag v1.0.0 and it publishes
repo.protequ.com/pq/pqacme:1.0.0 plus :latest; push v1.0.0-rc.1 and it publishes the
version tag only.
name: Release adapter image
on:
push:
tags:
- "v*.*.*"
workflow_dispatch:
inputs:
version:
description: "Semver version (example: 1.0.0-rc.1)"
required: true
type: string
permissions:
contents: read
env:
REGISTRY: repo.protequ.com
NAMESPACE: pq
IMAGE: pqacme
jobs:
release:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Resolve version and tags
id: rel
shell: bash
run: |
if [ -n "${{ inputs.version }}" ]; then
VERSION="${{ inputs.version }}"
else
VERSION="${GITHUB_REF_NAME#v}"
fi
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+([-.][0-9A-Za-z.-]+)?$ ]]; then
echo "::error::Invalid semver: $VERSION"
exit 1
fi
REF="${REGISTRY}/${NAMESPACE}/${IMAGE}"
TAGS="${REF}:${VERSION}"
# A prerelease (version contains "-") is published under its own tag
# only — it must never move the stable `latest` pointer.
case "$VERSION" in
*-*) echo "prerelease: not tagging latest" ;;
*) TAGS="${TAGS}"$'\n'"${REF}:latest" ;;
esac
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
{
echo "tags<<EOF"
echo "$TAGS"
echo "EOF"
} >> "$GITHUB_OUTPUT"
- name: Log in to the Protequ registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ secrets.PQ_REGISTRY_USERNAME }}
password: ${{ secrets.PQ_REGISTRY_TOKEN }}
- name: Set up Buildx
uses: docker/setup-buildx-action@v3
- name: Build and push
id: build
uses: docker/build-push-action@v6
with:
context: .
file: ./Dockerfile
platforms: linux/amd64
push: true
tags: ${{ steps.rel.outputs.tags }}
build-args: |
NUGET_SOURCE_URL=https://repo.protequ.com/api/packages/PQ/nuget/index.json
NUGET_SOURCE_USER=${{ secrets.PQ_REGISTRY_USERNAME }}
NUGET_SOURCE_PASS=${{ secrets.PQ_REGISTRY_TOKEN }}
- name: Summary
shell: bash
run: |
{
echo "## ${IMAGE} ${{ steps.rel.outputs.version }}"
echo "- Digest: \`${{ steps.build.outputs.digest }}\`"
echo "### Published tags"
while IFS= read -r t; do [ -n "$t" ] && echo "- \`$t\`"; done <<< "${{ steps.rel.outputs.tags }}"
} >> "$GITHUB_STEP_SUMMARY"
Optional, recommended: a vulnerability gate before the image reaches customers. Protequ's own pipeline fails the release on any fixable CRITICAL finding:
docker run --rm ghcr.io/aquasecurity/trivy:0.58.1 image \ --severity CRITICAL --ignore-unfixed --exit-code 1 \ "repo.protequ.com/pq/pqacme@${DIGEST}"Not required to publish, but a customer's security review will ask.
Manual push¶
For a one-off or a first trial, without CI:
docker build -t repo.protequ.com/pq/pqacme:1.0.0 --build-arg NUGET_SOURCE_URL=https://repo.protequ.com/api/packages/PQ/nuget/index.json --build-arg NUGET_SOURCE_USER=<user> --build-arg NUGET_SOURCE_PASS=<token> .
For a stable release, also move latest:
docker tag repo.protequ.com/pq/pqacme:1.0.0 repo.protequ.com/pq/pqacme:latest && docker push repo.protequ.com/pq/pqacme:latest
Verifying the publish¶
1. The tag exists
2. The labels are what you expect — the reliable check is to pull and inspect:
docker pull repo.protequ.com/pq/pqacme:1.0.0 && docker inspect repo.protequ.com/pq/pqacme:1.0.0 --format '{{json .Config.Labels}}'
To reproduce exactly what the installer does — manifest, then config blob:
curl -su "<user>:<token>" -H "Accept: application/vnd.oci.image.index.v1+json, application/vnd.oci.image.manifest.v1+json" https://repo.protequ.com/v2/pq/pqacme/manifests/latest
Take .config.digest from that response (or from the linux/amd64 child manifest if it is an
index) and fetch the blob:
.config.Labels in that blob is the exact input the catalog reads.
3. It appears in the installer — run the PQ installer's adapter selection step. Your adapter should be listed under its category with the title and description from your labels.
Troubleshooting¶
| Symptom | Cause |
|---|---|
unauthorized: authentication required on push |
Token missing, expired, or not passed. Re-run docker login; in CI check the secret name. |
denied: requested access to the resource is denied |
Your account has no write access to pq/, or you pushed to a different namespace. |
| Image pushed, but absent from the installer catalog | Almost always org.protequ.adapter="true" missing or misspelled. Check with docker inspect. Also confirm the repository is under pq/. |
| Adapter shows up flagged as a prerelease | Only prerelease tags exist — there is no latest. Publish a stable version. |
| A label change had no effect | Labels live in the image config. Retagging an existing digest does nothing; rebuild and push. And if you only pushed the version tag, latest still resolves to the old digest. |
| Buildx TLS error against the registry | Only on hosts that reach the registry over an internal, non-TLS path. Add an insecure entry for repo.protequ.com to your buildkitd config. Not needed from GitHub-hosted runners. |
dotnet restore cannot find Pq.Adapters.Framework |
The feed was not added inside the build stage, or the credentials were not passed as build args. |