Skip to content

Commit a38b6c1

Browse files
GuanzhouSongCopilot
andcommitted
Make package generation and GPG signing optional in the CD build
Forks could not run the deployment workflow cleanly: the GPG import step ran unconditionally, so a repository without GPG_PRIVATE_KEY got a failed step on every run. The continue-on-error that hid it was also a hazard upstream, where a broken key would silently republish the site with an unsigned APT repository in place of a signed one and break apt-get update for every client pinned with signed-by. Resolve two independent feature flags up front, since the secrets context is not readable from a step-level if condition: - Package repository generation follows the BUILD_PACKAGES variable, and defaults on for this repository and off for forks, so a fork build only exercises the site and Pages publish. - Signing follows the presence of GPG_PRIVATE_KEY. When the key is absent the import step is skipped rather than failed; when it is present the import must succeed, and the artifact check now asserts that InRelease, Release.gpg and the exported keyring were produced. Document the fork workflow and both settings in the readme. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Guanzhou Song <guanzhou.song@gmail.com>
1 parent 6762496 commit a38b6c1

3 files changed

Lines changed: 147 additions & 18 deletions

File tree

‎.github/scripts/download_packages.sh‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,13 @@ sign_deb_package() {
1515
if [ -n "$GPG_FINGERPRINT" ] && [ -f "$package_file" ]; then
1616
echo " Signing DEB package: $(basename "$package_file")"
1717
if command -v dpkg-sig >/dev/null 2>&1; then
18-
dpkg-sig --sign builder --gpg-options "--default-key $GPG_FINGERPRINT" "$package_file" || echo " Warning: Could not sign $(basename "$package_file")"
18+
# Per-package signatures are not what apt verifies (it checks the signed
19+
# Release file), so a failure here is reported rather than fatal. It is
20+
# still surfaced as a workflow annotation so a broken signing key cannot
21+
# scroll past unnoticed in the log.
22+
dpkg-sig --sign builder --gpg-options "--default-key $GPG_FINGERPRINT" "$package_file" || echo "::warning::Could not sign $(basename "$package_file")"
1923
else
20-
echo " Warning: dpkg-sig not available, skipping DEB package signing"
24+
echo "::warning::dpkg-sig not available, skipping DEB package signing"
2125
fi
2226
fi
2327
}

‎.github/workflows/continuous-deployment.yml‎

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -27,33 +27,106 @@ jobs:
2727
steps:
2828
- name: Checkout source
2929
uses: actions/checkout@v7
30-
- name: Install required packages
30+
- name: Resolve optional build features
31+
# The static site is always built. Mirroring the release packages and
32+
# signing them are separate opt-outs so that a fork with no secrets can
33+
# still run this workflow end to end.
34+
id: features
35+
env:
36+
# The `secrets` context is not readable from a step-level `if:`, so
37+
# the presence of the signing key has to be resolved into a step
38+
# output first. Binding the secret to this one step also keeps it out
39+
# of every other step's environment.
40+
GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }}
41+
# A fork has neither the signing key nor a reason to spend several
42+
# minutes mirroring release assets, so package generation defaults
43+
# off outside this repository. `BUILD_PACKAGES` overrides it either
44+
# way - but note that turning it off upstream publishes a site with
45+
# no /deb, /rpm and no keyring, which breaks `apt-get update` for
46+
# everyone already pointed at the repository.
47+
BUILD_PACKAGES: ${{ vars.BUILD_PACKAGES }}
48+
IS_UPSTREAM: ${{ github.repository == 'documentdb/documentdb.github.io' }}
49+
run: |
50+
set -euo pipefail
51+
requested=$(printf '%s' "$BUILD_PACKAGES" | tr '[:upper:]' '[:lower:]')
52+
case "$requested" in
53+
true|false) packages="$requested" ;;
54+
'') packages="$IS_UPSTREAM" ;;
55+
*)
56+
echo "::error::BUILD_PACKAGES must be 'true' or 'false' (got '$BUILD_PACKAGES')"
57+
exit 1
58+
;;
59+
esac
60+
61+
if [ "$packages" = 'true' ] && [ -n "$GPG_PRIVATE_KEY" ]; then
62+
sign=true
63+
else
64+
sign=false
65+
fi
66+
67+
echo "packages=$packages" >> "$GITHUB_OUTPUT"
68+
echo "sign=$sign" >> "$GITHUB_OUTPUT"
69+
70+
{
71+
echo "### Build configuration"
72+
echo ""
73+
echo "| Feature | Enabled | Controlled by |"
74+
echo "| --- | --- | --- |"
75+
echo "| Static site | true | always built |"
76+
echo "| Package repositories | $packages | \`BUILD_PACKAGES\` variable |"
77+
echo "| Package signing | $sign | \`GPG_PRIVATE_KEY\` secret |"
78+
} >> "$GITHUB_STEP_SUMMARY"
79+
80+
if [ "$packages" = 'true' ] && [ "$sign" != 'true' ]; then
81+
echo "::warning::GPG_PRIVATE_KEY is not set - the package repositories will be published unsigned."
82+
fi
83+
- name: Install packaging tools
84+
if: steps.features.outputs.packages == 'true'
3185
run: |
3286
until sudo apt-get update; do sleep 1; done
3387
sudo apt-get install -y createrepo-c dpkg-dev dpkg-sig gnupg2 python3
3488
- name: Setup GPG
3589
id: import_gpg
90+
if: steps.features.outputs.sign == 'true'
91+
# Deliberately no `continue-on-error`: a key that is configured but
92+
# cannot be imported has to fail the run. Swallowing that error
93+
# republishes the site with an unsigned repository in place of a signed
94+
# one, which breaks `apt-get update` for every client pinned with
95+
# `signed-by`. Signing is optional; silently losing it is not.
3696
uses: crazy-max/ghaction-import-gpg@v7
3797
with:
3898
gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }}
39-
continue-on-error: true
40-
- name: Set GPG fingerprint and version config
99+
- name: Configure package build
100+
if: steps.features.outputs.packages == 'true'
101+
env:
102+
SIGN: ${{ steps.features.outputs.sign }}
103+
FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }}
104+
KEY_ID: ${{ steps.import_gpg.outputs.keyid }}
105+
KEY_NAME: ${{ steps.import_gpg.outputs.name }}
106+
KEY_EMAIL: ${{ steps.import_gpg.outputs.email }}
107+
# Configure which DocumentDB release to mirror. Both can be
108+
# overridden by repository variables.
109+
DOCUMENTDB_VERSION: ${{ vars.DOCUMENTDB_VERSION || 'latest' }}
110+
MULTI_VERSION: ${{ vars.MULTI_VERSION || 'true' }}
41111
run: |
42-
# Configure GPG signing
43-
if [ -n "${{ steps.import_gpg.outputs.fingerprint }}" ]; then
44-
echo "GPG_FINGERPRINT=${{ steps.import_gpg.outputs.fingerprint }}" >> $GITHUB_ENV
45-
echo "✅ GPG key loaded successfully"
46-
echo " Fingerprint: ${{ steps.import_gpg.outputs.fingerprint }}"
47-
echo " Key ID: ${{ steps.import_gpg.outputs.keyid }}"
48-
echo " User ID: ${{ steps.import_gpg.outputs.name }} <${{ steps.import_gpg.outputs.email }}>"
112+
set -euo pipefail
113+
if [ "$SIGN" = 'true' ]; then
114+
if [ -z "$FINGERPRINT" ]; then
115+
echo "::error::The GPG key imported without a fingerprint; refusing to publish an unsigned repository."
116+
exit 1
117+
fi
118+
echo "GPG_FINGERPRINT=$FINGERPRINT" >> "$GITHUB_ENV"
119+
echo "GPG key loaded successfully"
120+
echo " Fingerprint: $FINGERPRINT"
121+
echo " Key ID: $KEY_ID"
122+
echo " User ID: $KEY_NAME <$KEY_EMAIL>"
49123
else
50-
echo "⚠️ No GPG key configured - packages will not be signed"
51-
echo " To enable signing, add GPG_PRIVATE_KEY to repository secrets"
124+
echo "No GPG key configured - packages will not be signed."
125+
echo "To enable signing, add GPG_PRIVATE_KEY to the repository secrets."
52126
fi
53-
54-
# Configure DocumentDB version (can be overridden by repository variables)
55-
echo "DOCUMENTDB_VERSION=${{ vars.DOCUMENTDB_VERSION || 'latest' }}" >> $GITHUB_ENV
56-
echo "MULTI_VERSION=${{ vars.MULTI_VERSION || 'true' }}" >> $GITHUB_ENV
127+
128+
echo "DOCUMENTDB_VERSION=$DOCUMENTDB_VERSION" >> "$GITHUB_ENV"
129+
echo "MULTI_VERSION=$MULTI_VERSION" >> "$GITHUB_ENV"
57130
- name: Setup Node.js
58131
uses: actions/setup-node@v7
59132
with:
@@ -108,12 +181,17 @@ jobs:
108181
exit 1
109182
fi
110183
- name: Download DocumentDB packages from latest release
184+
if: steps.features.outputs.packages == 'true'
111185
run: .github/scripts/download_packages.sh
112186
- name: Verify generated package components
187+
if: steps.features.outputs.packages == 'true'
188+
env:
189+
SIGN: ${{ steps.features.outputs.sign }}
113190
run: |
114191
set -euo pipefail
115192
python3 - <<'PY'
116193
import json
194+
import os
117195
from pathlib import Path
118196
119197
release_info = Path("out/packages/release-info.json")
@@ -150,6 +228,28 @@ jobs:
150228
release_text = release_file.read_text()
151229
if "deb13" not in release_text:
152230
raise SystemExit("deb13 assets exist but deb13 is missing from the APT Release file")
231+
232+
# A run that imported a signing key must not publish an unsigned
233+
# repository: apt rejects a suite whose InRelease/Release.gpg vanished,
234+
# so a silently skipped signature is a client-visible outage rather
235+
# than a cosmetic regression.
236+
if os.environ.get("SIGN") == "true":
237+
if any(name.endswith(".deb") for name in assets):
238+
for artifact in (
239+
Path("out/deb/dists/stable/Release.gpg"),
240+
Path("out/deb/dists/stable/InRelease"),
241+
Path("out/documentdb-archive-keyring.gpg"),
242+
):
243+
if not artifact.exists():
244+
raise SystemExit(
245+
f"Signing was enabled but {artifact} was not produced"
246+
)
247+
248+
# RPM metadata signing is best-effort inside the download script,
249+
# so surface it as a warning instead of failing the deployment.
250+
for repomd in sorted(Path("out/rpm").glob("*/repodata/repomd.xml")):
251+
if not Path(f"{repomd}.asc").exists():
252+
print(f"::warning::{repomd} was not signed")
153253
PY
154254
- name: Upload artifact
155255
uses: actions/upload-pages-artifact@v5

‎readme.md‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,31 @@ Documentation articles and API reference content are managed in a separate repos
112112
> - API reference content (`api-reference/`)
113113
>
114114
115+
### Testing the Deployment Workflow in a Fork
116+
117+
The [deployment workflow](.github/workflows/continuous-deployment.yml) builds the static site, mirrors the DocumentDB release packages into APT and YUM repositories, signs those repositories, and publishes everything to GitHub Pages.
118+
119+
Only the static site build is mandatory. Packaging and signing are resolved automatically at the start of the run, so a fork with no repository secrets can exercise the whole workflow:
120+
121+
1. Enable GitHub Pages in your fork (**Settings** > **Pages** > **Source**: *GitHub Actions*)
122+
123+
1. Push to `main` in your fork, or run the workflow manually from the **Actions** tab
124+
125+
1. Check the run summary, which reports which optional features were enabled and why
126+
127+
The two optional halves are controlled independently:
128+
129+
| Setting | Type | Default | Effect when enabled |
130+
| --- | --- | --- | --- |
131+
| `BUILD_PACKAGES` | Variable | On in `documentdb/documentdb.github.io`, off in forks | Downloads the release `.deb` and `.rpm` assets and builds the APT and YUM repositories |
132+
| `GPG_PRIVATE_KEY` | Secret | *unset* | Signs the APT `Release` file and the RPM metadata, and publishes `documentdb-archive-keyring.gpg` |
133+
| `DOCUMENTDB_VERSION` | Variable | `latest` | Release tag the packages are mirrored from |
134+
135+
Set `BUILD_PACKAGES` to `true` in your fork if you specifically want to test the packaging path; signing is then skipped unless you also add your own `GPG_PRIVATE_KEY`.
136+
137+
> [!IMPORTANT]
138+
> Signing is optional, but it never fails quietly. When `GPG_PRIVATE_KEY` is set, the run fails if the key cannot be imported or the signatures are not produced. Publishing an unsigned repository over a signed one breaks `apt-get update` for every client that already trusts the keyring.
139+
115140
## Content Configuration
116141
117142
Documentation content is automatically compiled during builds from external repositories. The mapping is configured in [content.config.json](content.config.json).

0 commit comments

Comments
 (0)