GitHub
Before connecting an organization, verify the authorized account, selected repositories, permissions, manifest binding and revocation behavior. A repository read or manifest check does not imply that AppsCert creates pull requests or deploys a change.
Single sign-on
Identity-provider setup depends on the selected tenant, callback configuration and membership rules. Confirm the exact provider and test login, logout, tenant mismatch and revoked membership before treating an SSO path as accepted for an environment.
Apple Pay and account setup
Apple certificate types, merchant accounts and domain association have separate requirements. A setup screen or successful API response alone does not establish Apple-side acceptance. See Apple Pay certificate types.
Other connectors and tools
PSP and vault support depends on the provider and authorized scope chosen for an environment. Do not place provider secrets in a public issue, URL or example. Confirm the operation, least-privilege access and recovery path before connecting a service.
Private releases and installation
The current client artifacts are published as versioned GitHub Release assets in private repositories. Sign in with a GitHub account that has repository read access before opening a release. An anonymous 404 is expected for these private links; it does not mean the release is missing. The SDKs are not published to npm or RubyGems, and the Action is not published to the GitHub Marketplace.
If you need repository access, email AppsCert support with the artifact and version you need and the GitHub account that should receive read access. Do not include API keys, OIDC assertions, workload tokens, passwords, private keys or certificate files in the request.
JavaScript SDK and CLI · 1.0.0
Open the private JavaScript release. Download appscert-sdk-1.0.0.tgz, appscert-1.0.0.tgz, and SHA256SUMS from its assets. Authenticate to the private release first. Verify the checksums, then install both tarballs from your existing consumer project; the block stops before npm install if any checksum fails and does not publish to npm:
set -euo pipefail
gh auth status
PACK_DIR="$(mktemp -d)"
gh release download 1.0.0 --repo jojoyaco/appscert-client-js --pattern 'appscert-sdk-1.0.0.tgz' --pattern 'appscert-1.0.0.tgz' --pattern SHA256SUMS --dir "$PACK_DIR"
verify_sha256sums() {
local dir="$1"
if command -v sha256sum >/dev/null 2>&1; then
(cd "$dir" && sha256sum -c SHA256SUMS)
elif command -v shasum >/dev/null 2>&1; then
(cd "$dir" && shasum -a 256 -c SHA256SUMS)
else
echo "Need sha256sum or shasum to verify release assets" >&2
return 1
fi
}
verify_sha256sums "$PACK_DIR"
npm install --no-save \
"$PACK_DIR/appscert-sdk-1.0.0.tgz" \
"$PACK_DIR/appscert-1.0.0.tgz"The v1 client needs an already-authorized workload token; the CLI also retains its separate legacy API-key mode.
Swift SDK · 1.0.0
Open the private Swift release. The supported SwiftPM dependency resolves the exact private Git tag; configure GitHub Git access first (gh auth status, then gh auth setup-gitif SwiftPM cannot use your authenticated Git session):
.package(
url: "https://github.com/jojoyaco/appscert-client-swift.git",
exact: "1.0.0"
)Add that package to the consumer target and import AppsCertSDK. The v1 client accepts an opaque workload token; do not embed one in a mobile app or browser. This SwiftPM library is not an Xcode plugin.
GitHub Action · 1.0.0
Open the private Action release. The complete file below runs on push to main or workflow_dispatch. GitHub lets a manual dispatch select a branch or tag (see workflow_dispatch); the job-level condition also requires refs/heads/main and an allowed event; a non-main run is skipped before a runner starts. This condition prevents accidental use of another ref, but is not a security boundary against someone who can change this workflow. The official Actions remain pinned to immutable release commit SHAs.
Protect the private-release read credential before using this example. APPSCERT_RELEASE_READ_TOKEN is consumed to download private release assets before the AppsCert OIDC exchange. The AppsCert binding is checked only during that later exchange; it does not protect this GitHub credential. The read token must be authorized for both private release repositories, and the default GITHUB_TOKEN cannot read other private repositories.
- Preferred when supported: before storing the token, create the
appscert-release-readGitHub Environment in repository Settings and set its deployment-branch rule to the approvedmainbranch. Verify the rule in GitHub Settings first, then storeAPPSCERT_RELEASE_READ_TOKENas an Environment secret and the non-secret AppsCert settings as trusted Environment variables. Merely writingenvironment: appscert-release-readin YAML creates a missing Environment without protection rules or secrets; the name alone is not protection. - GitHub Free does not provide configurable Environments or Environment secrets for private repositories. For private repositories, Environment and deployment-branch controls depend on the plan; GitHub documents branch restrictions for Pro/Team and Environment secrets for Pro/Team/Enterprise. Required reviewers on Free/Pro/Team are available only for public repositories, so do not assume a private approval gate exists. Check the exact repository plan and Settings before using an Environment. If the needed private protections are unavailable, use a dedicated private trusted automation repository instead. In that fallback, remove the job's
environmentline and ask support to bind the environment asnone. ConfigureAPPSCERT_API_URL,APPSCERT_OIDC_AUDIENCEandAPPSCERT_EXPECTED_SCOPESas trusted repository variables, not event inputs. Store a repository secret only if every user with write access is trusted to access the private assets; GitHub documents that write access can expose repository secrets. If that cannot be guaranteed, do not run this workflow. See GitHub's Environment availability, deployment-branch rules and secret-use guidance.
Keep the workflow definition on protected main with reviewed changes, and do not add pull-request triggers or check out and execute untrusted PR code in this job. A workflow condition cannot protect a secret from a hostile workflow definition; people who can bypass review or branch rules must also be trusted with the credential. For the managed binding, provide the exact repository and numeric IDs, ref refs/heads/main, workflow reference OWNER/REPOSITORY/.github/workflows/appscert-plan.yml@refs/heads/main, environment (appscert-release-read or none only for the stated fallback), audience and approved scopes. Do not request wildcard refs or workflow paths; support must confirm the exact binding and audience before enabling the exchange. See managed workload onboarding.
name: AppsCert private release plan
on:
push:
branches:
- main
workflow_dispatch: {}
jobs:
appscert-plan:
# This job guard is not a boundary against an untrusted workflow change.
if: >-
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
environment: appscert-release-read
permissions:
contents: read
id-token: write
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
- name: Download and verify AppsCert releases
shell: bash
env:
GH_TOKEN: ${{ secrets.APPSCERT_RELEASE_READ_TOKEN }}
run: |
set -euo pipefail
gh auth status
action_dir="$RUNNER_TEMP/appscert-action"
packages_dir="$RUNNER_TEMP/appscert-packages"
mkdir -p "$action_dir" "$packages_dir"
verify_sha256sums() {
local dir="$1"
if command -v sha256sum >/dev/null 2>&1; then
(cd "$dir" && sha256sum -c SHA256SUMS)
elif command -v shasum >/dev/null 2>&1; then
(cd "$dir" && shasum -a 256 -c SHA256SUMS)
else
echo "Need sha256sum or shasum to verify release assets" >&2
return 1
fi
}
gh release download 1.0.0 --repo jojoyaco/appscert-github-action --pattern 'appscert-github-action-1.0.0.tar.gz' --pattern SHA256SUMS --dir "$action_dir"
gh release download 1.0.0 --repo jojoyaco/appscert-client-js --pattern 'appscert-sdk-1.0.0.tgz' --pattern 'appscert-1.0.0.tgz' --pattern SHA256SUMS --dir "$packages_dir"
verify_sha256sums "$action_dir"
verify_sha256sums "$packages_dir"
tar -xzf "$action_dir/appscert-github-action-1.0.0.tar.gz" -C "$GITHUB_WORKSPACE"
- name: Exchange the pre-approved GitHub identity
id: exchange
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
env:
APPSCERT_API_URL: ${{ vars.APPSCERT_API_URL }}
APPSCERT_OIDC_AUDIENCE: ${{ vars.APPSCERT_OIDC_AUDIENCE }}
APPSCERT_EXPECTED_SCOPES: ${{ vars.APPSCERT_EXPECTED_SCOPES }}
with:
script: |
const env = process.env;
const officialIssuer = 'https://token.actions.githubusercontent.com';
const approvedApiOrigin = 'https://appscert.com';
function fail() {
throw new Error('The approved workload exchange could not be validated.');
}
function parseScopes(value) {
if (typeof value !== 'string' || value.length === 0) return null;
const values = value.split(',');
if (values.some((scope) => !scope || scope.trim() !== scope)) return null;
if (new Set(values).size !== values.length) return null;
return values.sort();
}
try {
if (!['push', 'workflow_dispatch'].includes(context.eventName)) fail();
if (!context.repo.owner || !context.repo.repo) fail();
const repository = context.repo.owner + '/' + context.repo.repo;
const audience = env.APPSCERT_OIDC_AUDIENCE;
const expectedScopes = parseScopes(env.APPSCERT_EXPECTED_SCOPES);
if (!audience || audience.length > 256 || /[\r\n]/.test(audience) || !expectedScopes) fail();
const apiBase = new URL(env.APPSCERT_API_URL || '');
if (apiBase.origin !== approvedApiOrigin || apiBase.pathname !== '/' || apiBase.search || apiBase.hash || apiBase.username || apiBase.password) fail();
const apiEndpoint = new URL('/api/v1/auth/exchange', approvedApiOrigin);
// The official toolkit uses the runner-provided OIDC URL and masks this JWT.
const assertion = await core.getIDToken(audience);
if (typeof assertion !== 'string' || assertion.length > 8192 || !/^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(assertion)) fail();
const exchangeResponse = await fetch(apiEndpoint, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ assertion }),
redirect: 'error',
signal: AbortSignal.timeout(10_000),
});
if (!exchangeResponse.ok) fail();
const result = await exchangeResponse.json();
const token = result && result.access_token;
if (typeof token !== 'string' || !/^[A-Za-z0-9_-]{43}$/.test(token)) fail();
if (result.token_type !== 'Bearer' || !Number.isSafeInteger(result.expires_in) || result.expires_in < 1 || result.expires_in > 3600) fail();
if (result.actor !== 'workload' || result.issuer !== officialIssuer || result.repository !== repository) fail();
const actualScopes = parseScopes(result.scope);
if (!actualScopes || JSON.stringify(actualScopes) !== JSON.stringify(expectedScopes)) fail();
// Mask the AppsCert credential before exporting it to later steps.
core.setSecret(token);
core.setOutput('token', token);
} catch {
core.setFailed('AppsCert OIDC exchange failed; no token was exported.');
}
- name: Create an AppsCert plan
uses: ./appscert-github-action-1.0.0
with:
mode: plan
api-url: ${{ vars.APPSCERT_API_URL }}
workload-token: ${{ steps.exchange.outputs.token }}
input-file: inputs/plan.json
cli-package: ${{ runner.temp }}/appscert-packages/appscert-1.0.0.tgz
sdk-package: ${{ runner.temp }}/appscert-packages/appscert-sdk-1.0.0.tgzGitHub Actions external access for this private repository is set to none: do not use uses: jojoyaco/appscert-github-action@1.0.0 from an arbitrary repository. The Action still accepts an already-issued workload token; the preceding workflow step performs the exchange and exports only its masked short-lived token. The exchange does not register an identity. The example uses the official core.getIDToken(audience) toolkit API, which uses the runner-provided OIDC request URL and masks the returned JWT; it does not guess or pin an OIDC host. GitHub documents that id-token: write only permits requesting a token; it does not grant repository write access. The AppsCert request remains fixed to https://appscert.com, rejects redirects and times out after 10 seconds. For a managed non-production deployment, change the API-origin pin only after verifying the destination; do not derive it from event data. See the official OIDC reference, Actions toolkit OIDC API, github-script v9.0.0 release, masking guidance and step-output docs. The checkout v7.0.1 release is pinned above. See managed workload onboarding.
Fastlane plugin · 1.1.0
Open the private Fastlane release. From the consumer project root, download all release assets so the complete SHA256SUMS manifest can be checked, then extract the source archive under vendor/private. It contains the gemspec needed by Bundler:
set -euo pipefail
gh auth status
PACK_DIR="$(mktemp -d)"
mkdir -p vendor/private
gh release download 1.1.0 --repo jojoyaco/appscert-fastlane --pattern 'fastlane-plugin-appscert-1.1.0.gem' --pattern 'appscert-fastlane-1.1.0.tar.gz' --pattern SHA256SUMS --dir "$PACK_DIR"
verify_sha256sums() {
local dir="$1"
if command -v sha256sum >/dev/null 2>&1; then
(cd "$dir" && sha256sum -c SHA256SUMS)
elif command -v shasum >/dev/null 2>&1; then
(cd "$dir" && shasum -a 256 -c SHA256SUMS)
else
echo "Need sha256sum or shasum to verify release assets" >&2
return 1
fi
}
verify_sha256sums "$PACK_DIR"
tar -xzf "$PACK_DIR/appscert-fastlane-1.1.0.tar.gz" -C vendor/privateAdd the local release source to the consumer Gemfile:
gem "fastlane-plugin-appscert", path: "vendor/private/appscert-fastlane-1.1.0"In the consumer Fastfile, load the plugin:
require "fastlane/plugin/appscert"Install dependencies, replace YOUR_LANE with the name of a lane already defined in your Fastfile, then run it:
bundle install
bundle exec fastlane YOUR_LANEProvide APPSCERT_API_KEY through the runner's secret manager and, if needed, set APPSCERT_API_URL as trusted runner configuration (default https://appscert.com). Fastlane reads APPSCERT_API_URL for its API base URL. It uses the legacy CI API key flow; grant ci:read for list/check and add ci:download only when the lane downloads certificate or profile material. Create the key in Dashboard → API Keys and store it in your runner's secret manager, not in the Fastfile or repository.
The old /docs/fastlane and /docs/extension URLs still redirect to this section. Other historical Xcode, browser-extension and mobile guide URLs are redirects, not separate installation packages.
Choose the credential for the client
- Fastlane: a legacy API key created in Dashboard → API Keys. The least-privilege pair for its check-and-download lanes is
ci:readplusci:download; do not substitutefull. - Versioned API v1: a short-lived opaque workload token from a pre-registered GitHub Actions identity. It is not a human session or legacy API key; request managed setup in the API guide.
For the current endpoint contract and the managed identity request fields, continue to API and authentication.