Skip to content

Commit

Permalink
Support App Store Connect API keys
Browse files Browse the repository at this point in the history
Closes #71.
  • Loading branch information
chris-araman committed Dec 30, 2021
1 parent 4ff863d commit 65c6103
Show file tree
Hide file tree
Showing 7 changed files with 176 additions and 10 deletions.
32 changes: 32 additions & 0 deletions .github/workflows/checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,38 @@ jobs:
echo >&2 "error: expected action to fail, but it succeeded"
exit 1
missing-api-key-id-fails:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: ./
id: xcodebuild
with:
authentication-key-base64: 'Cg=='
authentication-key-issuer-id: '14sb3dw0-r3t1-83u1-g381-4k9sg1t3w8r2'
working-directory: fixtures/debug
continue-on-error: true
- if: steps.xcodebuild.outcome == 'success'
run: |
echo >&2 "error: expected action to fail, but it succeeded"
exit 1
missing-api-key-issuer-id-fails:
runs-on: macos-latest
steps:
- uses: actions/checkout@v2
- uses: ./
id: xcodebuild
with:
authentication-key-base64: 'Cg=='
authentication-key-id: 'JW271KX2u3'
working-directory: fixtures/debug
continue-on-error: true
- if: steps.xcodebuild.outcome == 'success'
run: |
echo >&2 "error: expected action to fail, but it succeeded"
exit 1
null-none-action:
runs-on: macos-latest
strategy:
Expand Down
58 changes: 51 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -142,7 +142,8 @@ if you think we could have figured it out but didn’t please open a ticket.

GitHub’s images have a **limited selection** of Xcodes.

- GitHub list what is available for the current 10.15 image [here][gha-xcode-list].
- GitHub list what is available for the current [10.15][gha-xcode-list-catalina]
and [11][gha-xcode-list-big-sur] images.
- We run a scheduled workflow to determine what is available [here][automated-list].

To install other versions first use [sinoru/actions-setup-xcode], then
Expand Down Expand Up @@ -174,29 +175,68 @@ This behavior cannot currently be disabled, PR welcome.

## Code Signing

> This feature requires macOS.

Code signing can be enabled with either an App Store Connect API key, or with a
certificate.

### Using an App Store Connect API Key

> This feature requires Xcode 13 or later.

[Create][create-api-key-instructions] an API key on
[App Store Connect][create-api-key]. Download your key and Base64-encode it:

```bash
base64 AuthKey_9XXXX9XXXX.p8
```

Create [GitHub Secrets][secrets] for your base64-encoded key, the key ID, and
the key's issuer ID. The IDs are displayed on App Store Connect.

```yaml
jobs:
build:
runs-on: macos-latest
steps:
- use: mxcl/xcodebuild@v1
with:
code-sign-certificate: ${{ secrets.CERTIFICATE_BASE64 }}
code-sign-certificate-passphrase: ${{ secrets.CERTIFICATE_PASSPHRASE}}
authentication-key-base64: ${{ secrets.APP_STORE_CONNECT_KEY_BASE64 }}
authentication-key-id: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
authentication-key-issuer-id: ${{ secrets.APP_STORE_CONNECT_KEY_ISSER_ID }}
```

> This feature requires macOS.
For more information on this method of code signing, please review the
["Distribute apps in Xcode with cloud signing"][cloud-signing] talk from WWDC21.

### Using a Specific Certificate

A code signing certificate can be installed to the macOS Keychain. It is
automatically removed from the Keychain in a post action.
Alternatively, if you have a code signing certificate you'd like to use, it can
be installed to the macOS Keychain. It is automatically removed from the
Keychain in a post action.

```yaml
jobs:
build:
runs-on: macos-latest
steps:
- use: mxcl/xcodebuild@v1
with:
code-sign-certificate: ${{ secrets.CERTIFICATE_BASE64 }}
code-sign-certificate-passphrase: ${{ secrets.CERTIFICATE_PASSPHRASE}}
```

To export your certificate from Xcode and Base64 encode it, follow
[these instructions][export]. Store any secrets, including certificates and
passphrases, in GitHub as [Encrypted Secrets][secrets].

### Specifying an Identity

You may specify a `code-sign-identity` to override any `CODE_SIGN_IDENTITY`
specified by your project.

### Disabling Code Signing

To disable code signing, you can specify `code-sign-identity: '-'`.
## Caveats
Expand Down Expand Up @@ -265,7 +305,11 @@ This action does not support Windows.
1. Create a [Pull Request](https://github.com/mxcl/xcodebuild/compare)

[automated-list]: https://flatgit.luolix.top/mxcl/.github/?filename=versions.json
[gha-xcode-list]: https://github.com/actions/virtual-environments/blob/main/images/macos/macos-10.15-Readme.md#xcode
[cloud-signing]: https://developer.apple.com/videos/play/wwdc2021/10204/
[create-api-key]: https://appstoreconnect.apple.com/access/api
[create-api-key-instructions]: https://developer.apple.com/documentation/appstoreconnectapi/creating_api_keys_for_app_store_connect_api
[gha-xcode-list-catalina]: https://github.com/actions/virtual-environments/blob/main/images/macos/macos-10.15-Readme.md#xcode
[gha-xcode-list-big-sur]: https://github.com/actions/virtual-environments/blob/main/images/macos/macos-11-Readme.md#xcode
[sinoru/actions-setup-xcode]: https://github.com/sinoru/actions-setup-xcode
[img]: https://raw.githubusercontent.com/mxcl/xcodebuild/gh-pages/XCResult.png
[secrets]: https://docs.github.com/en/actions/reference/encrypted-secrets
Expand Down
21 changes: 21 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,27 @@ inputs:
description: Enables code coverage
required: false
default: 'false'
authentication-key-base64:
description: |
A Base64-encoded authentication key issued by App Store Connect. If
specified, `xcodebuild`` will authenticate with the Apple Developer
website using this credential. The `authentication-key-id` and
`authentication-key-issuer-id` parameters are required.
required: false
authentication-key-id:
description: |
The key identifier associated with the App Store Conect authentication key
specified in `authentication-key-base64`. This string can be located in
the users and access details for your provider at
"https://appstoreconnect.apple.com".
required: false
authentication-key-issuer-id:
description: |
The App Store Connect issuer identifier associated with the authentication
key specified in `authentication-key-base64`. This string can be located
in the users and access details for your provider at
"https://appstoreconnect.apple.com".
required: false
code-sign-certificate:
description: |
A Base64-encoded certificate to be installed to the macOS Keychain for
Expand Down
4 changes: 2 additions & 2 deletions dist/index.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion dist/index.js.map

Large diffs are not rendered by default.

43 changes: 43 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import {
actionIsTestable,
createApiKeyFile,
createKeychain,
deleteApiKeyFile,
deleteKeychain,
getAction,
getConfiguration,
Expand Down Expand Up @@ -48,6 +50,8 @@ async function main() {
generateXcodeproj(reason)
}

const apiKeyArgs = await configureApiKey()

await configureKeychain()

await build(await getScheme())
Expand Down Expand Up @@ -103,6 +107,43 @@ async function main() {
}
}

async function configureApiKey(): Promise<string[] | false> {
const key = core.getInput('authentication-key-base64')
if (!key) return false

if (semver.lt(selected, '13.0.0')) {
core.notice(
'Ignoring authentication-key-base64 because it requires Xcode 13 or later.'
)
return false
}

const keyId = core.getInput('authentication-key-id')
const keyIssuerId = core.getInput('authentication-key-issuer-id')
if (!keyId || !keyIssuerId) {
throw new Error(
'authentication-key-base64 requires authentication-key-id and authentication-key-issuer-id.'
)
}

// The user should have already stored these as encrypted secrets, but we'll
// be paranoid on their behalf.
core.setSecret(key)
core.setSecret(keyId)
core.setSecret(keyIssuerId)

const keyPath = await createApiKeyFile(key)
return [
'-allowProvisioningUpdates',
'-authenticationKeyPath',
keyPath,
'-authenticationKeyID',
keyId,
'-authenticationKeyIssuerID',
keyIssuerId,
]
}

async function configureKeychain() {
const certificate = core.getInput('code-sign-certificate')
if (!certificate) return
Expand Down Expand Up @@ -142,6 +183,7 @@ async function main() {
if (identity) args = args.concat(identity)
if (verbosity() == 'quiet') args.push('-quiet')
if (configuration) args = args.concat(['-configuration', configuration])
if (apiKeyArgs) args = args.concat(apiKeyArgs)

args = args.concat([
'-resultBundlePath',
Expand Down Expand Up @@ -180,6 +222,7 @@ async function main() {
}

function post() {
deleteApiKeyFile()
deleteKeychain()
}

Expand Down
26 changes: 26 additions & 0 deletions src/lib.ts
Original file line number Diff line number Diff line change
Expand Up @@ -459,3 +459,29 @@ export function deleteKeychain(): void {
}
}
}

export async function createApiKeyFile(key: string): Promise<string> {
// Avoid using a well-known path.
const name = (await exec('/usr/bin/uuidgen')).trim()
core.setSecret(name)

// Unfortunately, the key must be stored on disk. We remove it in a post action that calls deleteApiKeyFile.
const keyPath = `${process.env.RUNNER_TEMP}/${name}.p8`
core.saveState('keyPath', keyPath)
core.info('Creating App Store Connect API key file')
fs.writeFileSync(keyPath, key, { encoding: 'base64' })

return keyPath
}

export function deleteApiKeyFile() {
const keyPath = core.getState('keyPath')
if (keyPath && fs.existsSync(keyPath)) {
core.info('Deleting App Store Connect API key file')
try {
fs.unlinkSync(keyPath)
} catch (error) {
core.error('Failed to delete App Store Connect API key file: ' + error)
}
}
}

0 comments on commit 65c6103

Please sign in to comment.