Skip to content

Commit 236fcf5

Browse files
committed
DEVTOOLS-16383: Create GitlabCI pipeline to release Mac builds
Adds a new Gitlab CI pipeline that releases cloudflared Mac builds and replaces the Teamcity adhoc job. This will build, sign and create a new Github release or add the artifacts to an existing release if the other jobs finish first.
1 parent 73a9980 commit 236fcf5

File tree

4 files changed

+182
-11
lines changed

4 files changed

+182
-11
lines changed

.gitlab-ci.yml

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
stages: [build, release]
2+
3+
default:
4+
id_tokens:
5+
VAULT_ID_TOKEN:
6+
aud: https://vault.cfdata.org
7+
8+
# This before_script is injected into every job that runs on master meaning that if there is no tag the step
9+
# will succeed but only write "No tag present - Skipping" to the console.
10+
.check_tag:
11+
before_script:
12+
- |
13+
# Check if there is a Git tag pointing to HEAD
14+
echo "Tag found: $(git tag --points-at HEAD | grep .)"
15+
if git tag --points-at HEAD | grep .; then
16+
echo "Tag found: $(git tag --points-at HEAD | grep .)"
17+
export "VERSION=$(git tag --points-at HEAD | grep .)"
18+
else
19+
echo "No tag present — skipping."
20+
exit 0
21+
fi
22+
23+
# -----------------------------------------------
24+
# Stage 1: Build on every PR
25+
# -----------------------------------------------
26+
build_cloudflared_macos: &build
27+
stage: build
28+
rules:
29+
- if: $CI_PIPELINE_SOURCE == "merge_request_event" && $CI_COMMIT_BRANCH != "master"
30+
when: always
31+
- when: never
32+
tags:
33+
- "macstadium-${RUNNER_ARCH}"
34+
parallel:
35+
matrix:
36+
- RUNNER_ARCH: [arm, intel]
37+
artifacts:
38+
paths:
39+
- artifacts/*
40+
script:
41+
- '[ "${RUNNER_ARCH}" = "arm" ] && export TARGET_ARCH=arm64'
42+
- '[ "${RUNNER_ARCH}" = "intel" ] && export TARGET_ARCH=amd64'
43+
- ARCH=$(uname -m)
44+
- echo ARCH=$ARCH - TARGET_ARCH=$TARGET_ARCH
45+
- ./.teamcity/mac/install-cloudflare-go.sh
46+
- export PATH="/tmp/go/bin:$PATH"
47+
- BUILD_SCRIPT=.teamcity/mac/build.sh
48+
- if [[ ! -x ${BUILD_SCRIPT} ]] ; then exit ; fi
49+
- set -euo pipefail
50+
- echo "Executing ${BUILD_SCRIPT}"
51+
- exec ${BUILD_SCRIPT}
52+
53+
# -----------------------------------------------
54+
# Stage 1: Build and sign only on releases
55+
# -----------------------------------------------
56+
build_and_sign_cloudflared_macos:
57+
<<: *build
58+
extends: .check_tag
59+
rules:
60+
- if: $CI_COMMIT_BRANCH == "master"
61+
when: always
62+
- when: never
63+
secrets:
64+
APPLE_DEV_CA_CERT:
65+
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/apple_dev_ca_cert/data@kv
66+
file: false
67+
CFD_CODE_SIGN_CERT:
68+
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_cert_v2/data@kv
69+
file: false
70+
CFD_CODE_SIGN_KEY:
71+
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_key_v2/data@kv
72+
file: false
73+
CFD_CODE_SIGN_PASS:
74+
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_code_sign_pass_v2/data@kv
75+
file: false
76+
CFD_INSTALLER_CERT:
77+
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_cert_v2/data@kv
78+
file: false
79+
CFD_INSTALLER_KEY:
80+
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_key_v2/data@kv
81+
file: false
82+
CFD_INSTALLER_PASS:
83+
vault: gitlab/cloudflare/tun/cloudflared/_branch/master/cfd_installer_pass_v2/data@kv
84+
file: false
85+
86+
# -----------------------------------------------
87+
# Stage 2: Release to Github after building and signing
88+
# -----------------------------------------------
89+
release_cloudflared_macos_to_github:
90+
stage: release
91+
image: docker-registry.cfdata.org/stash/tun/docker-images/cloudflared-ci/main:6-8616fe631b76-amd64@sha256:96f4fd05e66cec03e0864c1bcf09324c130d4728eef45ee994716da499183614
92+
extends: .check_tag
93+
dependencies:
94+
- build_and_sign_cloudflared_macos
95+
rules:
96+
- if: $CI_COMMIT_BRANCH == "master"
97+
when: always
98+
- when: never
99+
cache:
100+
paths:
101+
- .cache/pip
102+
variables:
103+
PIP_CACHE_DIR: "$CI_PROJECT_DIR/.cache/pip"
104+
KV_NAMESPACE: 380e19aa04314648949b6ad841417ebe
105+
KV_ACCOUNT: 5ab4e9dfbd435d24068829fda0077963
106+
secrets:
107+
KV_API_TOKEN:
108+
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_kv_api_token/data@kv
109+
file: false
110+
API_KEY:
111+
vault: gitlab/cloudflare/tun/cloudflared/_dev/cfd_github_api_key/data@kv
112+
file: false
113+
script:
114+
- python3 --version ; pip --version # For debugging
115+
- python3 -m venv venv
116+
- source venv/bin/activate
117+
- pip install pynacl==1.4.0 pygithub==1.55
118+
- echo $VERSION
119+
- echo $TAG_EXISTS
120+
- echo "Running release because tag exists."
121+
- make macos-release

.teamcity/mac/build.sh

Lines changed: 46 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ import_certificate() {
4949
echo -n -e ${CERTIFICATE_ENV_VAR} | base64 -D > ${CERTIFICATE_FILE_NAME}
5050
# we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error
5151
# will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below.
52-
local out=$(security import ${CERTIFICATE_FILE_NAME} -A 2>&1) || true
52+
local out=$(security import ${CERTIFICATE_FILE_NAME} -T /usr/bin/pkgbuild -A 2>&1) || true
5353
local exitcode=$?
5454
# delete the certificate from disk
5555
rm -rf ${CERTIFICATE_FILE_NAME}
@@ -68,6 +68,28 @@ import_certificate() {
6868
fi
6969
}
7070

71+
create_cloudflared_build_keychain() {
72+
# Reusing the private key password as the keychain key
73+
local PRIVATE_KEY_PASS=$1
74+
75+
# Create keychain only if it doesn't already exist
76+
if [ ! -f "$HOME/Library/Keychains/cloudflared_build_keychain.keychain-db" ]; then
77+
security create-keychain -p "$PRIVATE_KEY_PASS" cloudflared_build_keychain
78+
else
79+
echo "Keychain already exists: cloudflared_build_keychain"
80+
fi
81+
82+
# Append temp keychain to the user domain
83+
security list-keychains -d user -s cloudflared_build_keychain $(security list-keychains -d user | sed s/\"//g)
84+
85+
# Remove relock timeout
86+
security set-keychain-settings cloudflared_build_keychain
87+
88+
# Unlock keychain so it doesn't require password
89+
security unlock-keychain -p "$PRIVATE_KEY_PASS" cloudflared_build_keychain
90+
91+
}
92+
7193
# Imports private keys to the Apple KeyChain
7294
import_private_keys() {
7395
local PRIVATE_KEY_NAME=$1
@@ -83,7 +105,7 @@ import_private_keys() {
83105
echo -n -e ${PRIVATE_KEY_ENV_VAR} | base64 -D > ${PRIVATE_KEY_FILE_NAME}
84106
# we set || true here and for every `security import invoke` because the "duplicate SecKeychainItemImport" error
85107
# will cause set -e to exit 1. It is okay we do this because we deliberately handle this error in the lines below.
86-
local out=$(security import ${PRIVATE_KEY_FILE_NAME} -A -P "${PRIVATE_KEY_PASS}" 2>&1) || true
108+
local out=$(security import ${PRIVATE_KEY_FILE_NAME} -k cloudflared_build_keychain -P "$PRIVATE_KEY_PASS" -T /usr/bin/pkgbuild -A -P "${PRIVATE_KEY_PASS}" 2>&1) || true
87109
local exitcode=$?
88110
rm -rf ${PRIVATE_KEY_FILE_NAME}
89111
if [ -n "$out" ]; then
@@ -100,6 +122,9 @@ import_private_keys() {
100122
fi
101123
}
102124

125+
# Create temp keychain only for this build
126+
create_cloudflared_build_keychain "${CFD_CODE_SIGN_PASS}"
127+
103128
# Add Apple Root Developer certificate to the key chain
104129
import_certificate "Apple Developer CA" "${APPLE_DEV_CA_CERT}" "${APPLE_CA_CERT}"
105130

@@ -119,8 +144,8 @@ import_certificate "Developer ID Installer" "${CFD_INSTALLER_CERT}" "${INSTALLER
119144
if [[ ! -z "$CFD_CODE_SIGN_NAME" ]]; then
120145
CODE_SIGN_NAME="${CFD_CODE_SIGN_NAME}"
121146
else
122-
if [[ -n "$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
123-
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
147+
if [[ -n "$(security find-certificate -c "Developer ID Application" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)" ]]; then
148+
CODE_SIGN_NAME=$(security find-certificate -c "Developer ID Application" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Application:" | head -1)
124149
else
125150
CODE_SIGN_NAME=""
126151
fi
@@ -130,8 +155,8 @@ fi
130155
if [[ ! -z "$CFD_INSTALLER_NAME" ]]; then
131156
PKG_SIGN_NAME="${CFD_INSTALLER_NAME}"
132157
else
133-
if [[ -n "$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
134-
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
158+
if [[ -n "$(security find-certificate -c "Developer ID Installer" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)" ]]; then
159+
PKG_SIGN_NAME=$(security find-certificate -c "Developer ID Installer" cloudflared_build_keychain | cut -d'"' -f 4 -s | grep "Developer ID Installer:" | head -1)
135160
else
136161
PKG_SIGN_NAME=""
137162
fi
@@ -142,9 +167,16 @@ rm -rf "${TARGET_DIRECTORY}"
142167
export TARGET_OS="darwin"
143168
GOCACHE="$PWD/../../../../" GOPATH="$PWD/../../../../" CGO_ENABLED=1 make cloudflared
144169

170+
171+
# This allows apple tools to use the certificates in the keychain without requiring password input.
172+
# This command always needs to run after the certificates have been loaded into the keychain
173+
if [[ ! -z "$CFD_CODE_SIGN_PASS" ]]; then
174+
security set-key-partition-list -S apple-tool:,apple: -s -k "${CFD_CODE_SIGN_PASS}" cloudflared_build_keychain
175+
fi
176+
145177
# sign the cloudflared binary
146178
if [[ ! -z "$CODE_SIGN_NAME" ]]; then
147-
codesign -s "${CODE_SIGN_NAME}" -f -v --timestamp --options runtime ${BINARY_NAME}
179+
codesign --keychain $HOME/Library/Keychains/cloudflared_build_keychain.keychain-db -s "${CODE_SIGN_NAME}" -fv --options runtime --timestamp ${BINARY_NAME}
148180

149181
# notarize the binary
150182
# TODO: TUN-5789
@@ -165,11 +197,13 @@ tar czf "$FILENAME" "${BINARY_NAME}"
165197

166198
# build the installer package
167199
if [[ ! -z "$PKG_SIGN_NAME" ]]; then
200+
168201
pkgbuild --identifier com.cloudflare.${PRODUCT} \
169202
--version ${VERSION} \
170203
--scripts ${ARCH_TARGET_DIRECTORY}/scripts \
171204
--root ${ARCH_TARGET_DIRECTORY}/contents \
172205
--install-location /usr/local/bin \
206+
--keychain cloudflared_build_keychain \
173207
--sign "${PKG_SIGN_NAME}" \
174208
${PKGNAME}
175209

@@ -187,3 +221,8 @@ fi
187221
# cleanup build directory because this script is not ran within containers,
188222
# which might lead to future issues in subsequent runs.
189223
rm -rf "${TARGET_DIRECTORY}"
224+
225+
# cleanup the keychain
226+
security default-keychain -d user -s login.keychain-db
227+
security list-keychains -d user -s login.keychain-db
228+
security delete-keychain cloudflared_build_keychain

Makefile

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,10 @@ github-release:
237237
python3 github_release.py --path $(PWD)/built_artifacts --release-version $(VERSION)
238238
python3 github_message.py --release-version $(VERSION)
239239

240+
.PHONY: macos-release
241+
macos-release:
242+
python3 github_release.py --path $(PWD)/artifacts/ --release-version $(VERSION)
243+
240244
.PHONY: r2-linux-release
241245
r2-linux-release:
242246
python3 ./release_pkgs.py

github_release.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def assert_tag_exists(repo, version):
6161
raise Exception("Tag {} not found".format(version))
6262

6363

64-
def get_or_create_release(repo, version, dry_run=False):
64+
def get_or_create_release(repo, version, dry_run=False, is_draft=False):
6565
"""
6666
Get a Github Release matching the version tag or create a new one.
6767
If a conflict occurs on creation, attempt to fetch the Release on last time
@@ -81,8 +81,11 @@ def get_or_create_release(repo, version, dry_run=False):
8181
return
8282

8383
try:
84-
logging.info("Creating release %s", version)
85-
return repo.create_git_release(version, version, "")
84+
if is_draft:
85+
logging.info("Drafting release %s", version)
86+
else:
87+
logging.info("Creating release %s", version)
88+
return repo.create_git_release(version, version, "", is_draft)
8689
except GithubException as e:
8790
errors = e.data.get("errors", [])
8891
if e.status == 422 and any(
@@ -129,6 +132,10 @@ def parse_args():
129132
"--dry-run", action="store_true", help="Do not create release or upload asset"
130133
)
131134

135+
parser.add_argument(
136+
"--draft", action="store_true", help="Create a draft release"
137+
)
138+
132139
args = parser.parse_args()
133140
is_valid = True
134141
if not args.release_version:
@@ -292,7 +299,7 @@ def main():
292299
for filename in onlyfiles:
293300
binary_path = os.path.join(args.path, filename)
294301
assert_asset_version(binary_path, args.release_version)
295-
release = get_or_create_release(repo, args.release_version, args.dry_run)
302+
release = get_or_create_release(repo, args.release_version, args.dry_run, args.draft)
296303
for filename in onlyfiles:
297304
binary_path = os.path.join(args.path, filename)
298305
upload_asset(release, binary_path, filename, args.release_version, args.kv_account_id, args.namespace_id,

0 commit comments

Comments
 (0)