Skip to main content

AppsCert integration releases and setup

Install a released client from its private GitHub release, then use the credential type that matches that client. These releases are available to authorized GitHub accounts; they are not public registry packages.

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-read GitHub Environment in repository Settings and set its deployment-branch rule to the approved main branch. Verify the rule in GitHub Settings first, then store APPSCERT_RELEASE_READ_TOKEN as an Environment secret and the non-secret AppsCert settings as trusted Environment variables. Merely writing environment: appscert-release-read in 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 environment line and ask support to bind the environment as none. Configure APPSCERT_API_URL, APPSCERT_OIDC_AUDIENCE and APPSCERT_EXPECTED_SCOPES as 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.tgz

GitHub 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/private

Add 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_LANE

Provide 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:read plus ci:download; do not substitute full.
  • 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.

1f821d3