Skip to content

Commit 1c2ca1f

Browse files
committed
build: added script to sign releases
1 parent 0bc3a90 commit 1c2ca1f

4 files changed

Lines changed: 265 additions & 7 deletions

File tree

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
# 13. Release Artifacts
1+
# 13. Release
22

3-
This document defines the canonical artifact layout for phpMyFAQ releases.
3+
This document defines the canonical release artifact layout for phpMyFAQ and the local signing step for those artifacts.
44

55
## 13.1 Release directory
66

@@ -31,9 +31,9 @@ Examples:
3131
- `phpMyFAQ-nightly-2026-03-25.zip`
3232
- `phpMyFAQ-nightly-2026-03-25.tar.gz`
3333

34-
## 13.3 Reserved signing outputs
34+
## 13.3 Signing outputs
3535

36-
The signing phase will add these files to the same directory:
36+
The signing phase adds these files to the same directory:
3737

3838
- `SHA256SUMS`
3939
- `SHA256SUMS.asc`
@@ -57,9 +57,55 @@ build/release/4.2.0/hashes-4.2.0.json
5757
build/release/4.2.0/ARTIFACTS.txt
5858
```
5959

60-
## Notes
60+
## 13.5 Signing command
61+
62+
To generate checksums and signatures:
63+
64+
```bash
65+
./scripts/sign-release-artifacts.sh 4.2.0
66+
```
67+
68+
The signing helper creates:
69+
70+
- `SHA256SUMS`
71+
- `SHA256SUMS.asc`
72+
- `phpMyFAQ-<version>.zip.asc`
73+
- `phpMyFAQ-<version>.tar.gz.asc`
74+
75+
The helper also verifies the generated checksums and signatures before it exits.
76+
77+
## 13.6 Environment variables
78+
79+
Optional variables for GPG signing:
80+
81+
- `GPG_KEY_ID`
82+
- `GPG_PASSPHRASE`
83+
84+
Example:
85+
86+
```bash
87+
GPG_KEY_ID=0123456789ABCDEF \
88+
GPG_PASSPHRASE='secret' \
89+
./scripts/sign-release-artifacts.sh 4.2.0
90+
```
91+
92+
## 13.7 Local checksum-only mode
93+
94+
If no release key is available, generate and verify checksums only:
95+
96+
```bash
97+
SKIP_GPG=1 ./scripts/sign-release-artifacts.sh 4.2.0
98+
```
99+
100+
This mode creates:
101+
102+
- `SHA256SUMS`
103+
104+
It does not create detached signatures.
105+
106+
## 13.8 Notes
61107

62108
- The package payload is the `phpmyfaq/` directory prepared from a clean git checkout.
63109
- The helper installs production dependencies and runs the frontend production build before packaging.
64110
- TCPDF fonts and examples are removed from the packaged checkout, matching the existing release process.
65-
- Signing is intentionally handled in a later phase so artifact naming and layout stay stable first.
111+
- Use `./scripts/sign-release-artifacts.sh` to generate checksums and signatures.

mkdocs.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,5 +19,5 @@ nav:
1919
- 10. Developer documentation: 'development.md'
2020
- 11. Plugins: 'plugins.md'
2121
- 12. MCP Server: 'mcp-server.md'
22-
- 13. Release artifacts: 'release-artifacts.md'
22+
- 13. Release: 'release.md'
2323
- 14. Thank you!: 'thank-you.md'

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
"build:watch": "vite build --watch",
3737
"build:prod": "vite build",
3838
"release:artifacts": "./scripts/prepare-release-artifacts.sh",
39+
"release:sign": "./scripts/sign-release-artifacts.sh",
3940
"eslint": "eslint .",
4041
"lint": "prettier --check .",
4142
"lint:fix": "prettier --write .",

scripts/sign-release-artifacts.sh

Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
#!/bin/sh
2+
3+
#
4+
# For signing a release run:
5+
#
6+
# ./scripts/sign-release-artifacts.sh x.y.z
7+
#
8+
# The script will download the source code from branch and
9+
# it will create the 2 packages plus their MD5 hashes.
10+
#
11+
# This Source Code Form is subject to the terms of the Mozilla Public License,
12+
# v. 2.0. If a copy of the MPL was not distributed with this file, You can
13+
# obtain one at https://mozilla.org/MPL/2.0/.
14+
#
15+
# @package phpMyFAQ
16+
# @author Thorsten Rinne <thorsten@phpmyfaq.de>
17+
# @copyright 2026 phpMyFAQ Team
18+
# @license https://www.mozilla.org/MPL/2.0/ Mozilla Public License Version 2.0
19+
# @link https://www.phpmyfaq.de
20+
# @version 2026-03-26
21+
#
22+
23+
set -eu
24+
# shellcheck disable=SC3040
25+
if (set -o pipefail 2>/dev/null); then
26+
set -o pipefail
27+
fi
28+
IFS=$(printf ' \t\n')
29+
30+
# shellcheck disable=SC1007
31+
SCRIPT_DIR=$(CDPATH= cd -- "$(dirname "$0")" && pwd)
32+
# shellcheck disable=SC1007
33+
REPO_ROOT=$(CDPATH= cd -- "${SCRIPT_DIR}/.." && pwd)
34+
35+
if [ "$#" -gt 1 ]; then
36+
printf 'Usage: %s [version]\n' "$0" >&2
37+
exit 1
38+
fi
39+
40+
: "${PHP_BIN:=php}"
41+
: "${VERSION:=${1:-$(php "${REPO_ROOT}/scripts/get-version.php")}}"
42+
43+
RELEASE_DIR="${REPO_ROOT}/build/release/${VERSION}"
44+
ZIP_FILE="${RELEASE_DIR}/phpMyFAQ-${VERSION}.zip"
45+
TAR_FILE="${RELEASE_DIR}/phpMyFAQ-${VERSION}.tar.gz"
46+
SHA256_FILE="${RELEASE_DIR}/SHA256SUMS"
47+
SHA256_ASC_FILE="${RELEASE_DIR}/SHA256SUMS.asc"
48+
ZIP_ASC_FILE="${ZIP_FILE}.asc"
49+
TAR_ASC_FILE="${TAR_FILE}.asc"
50+
ARTIFACT_MANIFEST="${RELEASE_DIR}/ARTIFACTS.txt"
51+
52+
log() {
53+
printf '\n[%s] %s\n' "$(date '+%H:%M:%S')" "$*"
54+
}
55+
56+
fail() {
57+
printf '\n[ERROR] %s\n' "$*" >&2
58+
exit 1
59+
}
60+
61+
require_command() {
62+
command -v "$1" >/dev/null 2>&1 || fail "Required command '$1' not found in PATH"
63+
}
64+
65+
set_sha256_command() {
66+
if command -v sha256sum >/dev/null 2>&1; then
67+
SHA256_CMD='sha256sum'
68+
return
69+
fi
70+
71+
if command -v shasum >/dev/null 2>&1; then
72+
SHA256_CMD='shasum -a 256'
73+
return
74+
fi
75+
76+
fail "Neither sha256sum nor shasum is available"
77+
}
78+
79+
check_prerequisites() {
80+
require_command "${PHP_BIN}"
81+
set_sha256_command
82+
83+
[ -d "${RELEASE_DIR}" ] || fail "Release directory ${RELEASE_DIR} does not exist"
84+
[ -f "${ZIP_FILE}" ] || fail "Missing artifact ${ZIP_FILE}"
85+
[ -f "${TAR_FILE}" ] || fail "Missing artifact ${TAR_FILE}"
86+
87+
if [ "${SKIP_GPG:-0}" != "1" ]; then
88+
require_command gpg
89+
fi
90+
}
91+
92+
generate_checksums() {
93+
log "Generating SHA256SUMS"
94+
rm -f "${SHA256_FILE}"
95+
96+
(
97+
cd "${RELEASE_DIR}"
98+
${SHA256_CMD} "$(basename "${ZIP_FILE}")" > "${SHA256_FILE}"
99+
${SHA256_CMD} "$(basename "${TAR_FILE}")" >> "${SHA256_FILE}"
100+
)
101+
}
102+
103+
gpg_base_args() {
104+
if [ -n "${GPG_PASSPHRASE:-}" ]; then
105+
printf '%s\n' "--batch --yes --pinentry-mode loopback --passphrase ${GPG_PASSPHRASE}"
106+
return
107+
fi
108+
109+
printf '%s\n' "--batch --yes"
110+
}
111+
112+
gpg_local_user_args() {
113+
if [ -n "${GPG_KEY_ID:-}" ]; then
114+
printf '%s\n' "--local-user ${GPG_KEY_ID}"
115+
return
116+
fi
117+
118+
printf '%s\n' ""
119+
}
120+
121+
sign_artifacts() {
122+
if [ "${SKIP_GPG:-0}" = "1" ]; then
123+
log "SKIP_GPG=1 set; only SHA256SUMS was generated"
124+
return
125+
fi
126+
127+
log "Signing SHA256SUMS and release artifacts"
128+
rm -f "${SHA256_ASC_FILE}" "${ZIP_ASC_FILE}" "${TAR_ASC_FILE}"
129+
130+
GPG_ARGS="$(gpg_base_args)"
131+
GPG_USER_ARGS="$(gpg_local_user_args)"
132+
133+
# shellcheck disable=SC2086
134+
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${SHA256_ASC_FILE}" "${SHA256_FILE}"
135+
# shellcheck disable=SC2086
136+
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${ZIP_ASC_FILE}" "${ZIP_FILE}"
137+
# shellcheck disable=SC2086
138+
gpg ${GPG_ARGS} ${GPG_USER_ARGS} --armor --detach-sign --output "${TAR_ASC_FILE}" "${TAR_FILE}"
139+
}
140+
141+
verify_outputs() {
142+
log "Verifying checksums"
143+
(
144+
cd "${RELEASE_DIR}"
145+
if [ "${SHA256_CMD}" = "sha256sum" ]; then
146+
sha256sum -c "${SHA256_FILE}"
147+
else
148+
shasum -a 256 -c "${SHA256_FILE}"
149+
fi
150+
)
151+
152+
if [ "${SKIP_GPG:-0}" = "1" ]; then
153+
return
154+
fi
155+
156+
log "Verifying signatures"
157+
gpg --verify "${SHA256_ASC_FILE}" "${SHA256_FILE}"
158+
gpg --verify "${ZIP_ASC_FILE}" "${ZIP_FILE}"
159+
gpg --verify "${TAR_ASC_FILE}" "${TAR_FILE}"
160+
}
161+
162+
update_manifest() {
163+
if [ ! -f "${ARTIFACT_MANIFEST}" ]; then
164+
return
165+
fi
166+
167+
cat > "${ARTIFACT_MANIFEST}" <<EOF
168+
Release artifact layout for phpMyFAQ ${VERSION}
169+
170+
Directory:
171+
${RELEASE_DIR}
172+
173+
Artifacts:
174+
- $(basename "${ZIP_FILE}")
175+
- $(basename "${TAR_FILE}")
176+
- $(basename "${SHA256_FILE}")
177+
EOF
178+
179+
if [ -f "${RELEASE_DIR}/hashes-${VERSION}.json" ]; then
180+
printf '%s\n' "- hashes-${VERSION}.json" >> "${ARTIFACT_MANIFEST}"
181+
fi
182+
183+
cat >> "${ARTIFACT_MANIFEST}" <<EOF
184+
- ARTIFACTS.txt
185+
EOF
186+
187+
if [ "${SKIP_GPG:-0}" != "1" ]; then
188+
cat >> "${ARTIFACT_MANIFEST}" <<EOF
189+
- $(basename "${SHA256_ASC_FILE}")
190+
- $(basename "${ZIP_ASC_FILE}")
191+
- $(basename "${TAR_ASC_FILE}")
192+
EOF
193+
fi
194+
}
195+
196+
main() {
197+
check_prerequisites
198+
generate_checksums
199+
sign_artifacts
200+
verify_outputs
201+
update_manifest
202+
203+
log "Release signing outputs prepared in ${RELEASE_DIR}"
204+
printf ' - %s\n' "${SHA256_FILE}"
205+
206+
if [ "${SKIP_GPG:-0}" != "1" ]; then
207+
printf ' - %s\n' "${SHA256_ASC_FILE}" "${ZIP_ASC_FILE}" "${TAR_ASC_FILE}"
208+
fi
209+
}
210+
211+
main

0 commit comments

Comments
 (0)