Skip to content

PoC: binary xcframework release pipeline#439

Draft
wenxi-zeng wants to merge 12 commits into
mainfrom
poc/binary-xcframework-release-pipeline
Draft

PoC: binary xcframework release pipeline#439
wenxi-zeng wants to merge 12 commits into
mainfrom
poc/binary-xcframework-release-pipeline

Conversation

@wenxi-zeng

@wenxi-zeng wenxi-zeng commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Summary

  • Implements the ADR's five-stage build-public, gate-internal, publish-public model for distributing analytics-swift as a binary Swift package (.xcframework)
  • Replaces existing CI workflows with two new workflows: release-build.yml (build + attest) and publish.yml (verify clearance + publish to GitHub Release)
  • Internal clearance gate is a placeholder — assumed to exist on Twilio's internal pipeline

How it works

  1. Release build (triggered on version tag): builds xcframework for all platforms, computes sha256 digest, emits a consumer-verifiable build attestation via actions/attest-build-provenance, uploads artifact
  2. Internal clearance (not in this PR): ingests artifact, runs SCA/SAST/secret/malware/license gates, emits KMS-signed clearance attestation bound to the digest
  3. Publish (manual trigger after clearance): verifies build attestation + clearance + commit equality, fails closed on mismatch, uploads exact cleared bytes to GitHub Release

Known gaps

  • SPM git-based dependency resolution through Artifactory is not yet available (ADR Rule 3 requires it)
  • Internal clearance pipeline does not exist yet — placeholder only
  • Cross-tier transport (how artifact moves between GitHub and internal pipeline) is unspecified
  • Package.swift binary target update after release requires a separate commit

Test plan

  • Validate xcframework build step produces valid multi-platform framework
  • Verify actions/attest-build-provenance emits attestation for the zip digest
  • Verify publish workflow fails closed when digest doesn't match
  • Verify gh attestation verify works for consumers against the build attestation

@wenxi-zeng wenxi-zeng marked this pull request as draft June 12, 2026 22:24
Comment thread .github/workflows/publish.yml Outdated
Comment on lines +157 to +175
run: |
set -euo pipefail

VERSION="${{ inputs.version }}"
DOWNLOAD_URL="https://github.com/${{ github.repository }}/releases/download/${VERSION}/${{ env.ARTIFACT_NAME }}"

echo "Binary target URL: $DOWNLOAD_URL"
echo "Binary target checksum: ${{ env.ARTIFACT_DIGEST }}"
echo ""
echo "PLACEHOLDER: Update Package.swift to include:"
echo ""
echo " .binaryTarget("
echo " name: \"Segment\","
echo " url: \"$DOWNLOAD_URL\","
echo " checksum: \"${{ env.ARTIFACT_DIGEST }}\""
echo " )"
echo ""
echo "NOTE: This requires a commit to the repo after the release is created."
echo "The manifest update is a separate commit that references the released artifact."

@semgrep-code-segmentio-2 semgrep-code-segmentio-2 Bot Jun 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

🌟 Fixed in commit 1ddac56 🌟

Comment thread .github/workflows/publish.yml Outdated
Comment on lines +125 to +153
run: |
set -euo pipefail

VERSION="${{ inputs.version }}"

gh release create "$VERSION" \
"${{ env.ARTIFACT_NAME }}" \
--title "Version $VERSION" \
--notes "$(cat <<'EOF'
## Segment.xcframework $VERSION

Binary Swift package release.

### Consumer verification

Verify the build attestation (build-from-commit proof):
```
gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }}
```

### Checksum (for Package.swift binaryTarget)
```
${{ env.ARTIFACT_DIGEST }}
```
EOF
)"

echo "Published ${{ env.ARTIFACT_NAME }} to release $VERSION"

@semgrep-code-segmentio-2 semgrep-code-segmentio-2 Bot Jun 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

🥳 Fixed in commit 1ddac56 🥳

Comment on lines +59 to +77
run: |
set -euo pipefail

COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}')
EXPECTED_DIGEST="${{ inputs.artifact-digest }}"

echo "Computed digest: $COMPUTED_DIGEST"
echo "Expected digest: $EXPECTED_DIGEST"

if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "::error::DIGEST MISMATCH — artifact may have been tampered with"
echo "::error::Expected: $EXPECTED_DIGEST"
echo "::error::Got: $COMPUTED_DIGEST"
exit 1
fi

echo "Digest verified: $COMPUTED_DIGEST"
echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV"

@semgrep-code-segmentio-2 semgrep-code-segmentio-2 Bot Jun 12, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

🎉 Fixed in commit 1ddac56 🎉

wenxi-zeng and others added 2 commits July 2, 2026 13:33
…, publish-public)

Implements the ADR's five-stage model for distributing analytics-swift
as a binary Swift package. The build produces an xcframework with a
consumer-verifiable build attestation, and the publish workflow gates
on internal clearance before uploading to GitHub Releases.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add OIDC token exchange (github-actions-segmentio provider) to get a
  short-lived Artifactory read token
- Configure SPM mirrors to resolve through virtual-swift-thirdparty
- Pin all actions to commit SHAs, use macos-26 runner
- Rename release-build.yml to build.yml
- Clean up publish.yml placeholders

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@wenxi-zeng wenxi-zeng force-pushed the poc/binary-xcframework-release-pipeline branch from 3692453 to 1ddac56 Compare July 2, 2026 18:43
Comment on lines +88 to +109
run: |
set -euo pipefail
VERSION="${{ inputs.version }}"

gh release create "$VERSION" \
"${{ env.ARTIFACT_NAME }}" \
--title "Version $VERSION" \
--notes "## Segment.xcframework ${VERSION}

Binary Swift package release.

### Consumer verification

Verify the build attestation:
\`\`\`
gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }}
\`\`\`

### Checksum (for Package.swift binaryTarget)
\`\`\`
${{ env.ARTIFACT_DIGEST }}
\`\`\`"

@semgrep-code-segmentio-2 semgrep-code-segmentio-2 Bot Jul 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

🎉 Removed in commit b46b44a 🎉

Comment on lines +54 to +66
run: |
set -euo pipefail
COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}')
EXPECTED_DIGEST="${{ inputs.artifact-digest }}"

if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "::error::DIGEST MISMATCH — artifact may have been tampered with"
exit 1
fi

echo "Digest verified: $COMPUTED_DIGEST"
echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV"

@semgrep-code-segmentio-2 semgrep-code-segmentio-2 Bot Jul 2, 2026

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Using variable interpolation ${{...}} with github context data in a run: step could allow an attacker to inject their own code into the runner. This would allow them to steal secrets and code. github context data can have arbitrary user input and should be treated as untrusted. Instead, use an intermediate environment variable with env: to store the data and use the environment variable in the run: script. Be sure to use double-quotes the environment variable, like this: "$ENVVAR".

Removed in commit b46b44a

wenxi-zeng and others added 5 commits July 2, 2026 13:50
Change from tag-only trigger to push/PR/manual dispatch so we can
validate Artifactory dependency resolution on every build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The org policy only allows enterprise-owned or GitHub-created actions.
Use xcode-select directly since macOS runners have Xcode pre-installed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
macos-26 has no available runners. Use macos-15 (GitHub-hosted standard)
with Xcode 16.2 which ships on that image.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lows

No macOS runners available in the org. Add a lightweight workflow that
validates SPM dependency resolution through Artifactory via OIDC on
ubuntu-latest. Move build.yml and publish.yml to workflows-disabled/
until macOS runners are available.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Comment on lines +88 to +109
run: |
set -euo pipefail
VERSION="${{ inputs.version }}"

gh release create "$VERSION" \
"${{ env.ARTIFACT_NAME }}" \
--title "Version $VERSION" \
--notes "## Segment.xcframework ${VERSION}

Binary Swift package release.

### Consumer verification

Verify the build attestation:
\`\`\`
gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }}
\`\`\`

### Checksum (for Package.swift binaryTarget)
\`\`\`
${{ env.ARTIFACT_DIGEST }}
\`\`\`"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:

Untrusted workflow input inputs.version is interpolated directly into shell command in a run: step, allowing arbitrary code execution. Attackers can inject shell metacharacters via workflow dispatch to execute malicious commands on the runner.

More details about this

The run: step directly interpolates ${{ inputs.version }} into a shell command without protection. An attacker could exploit this by submitting a workflow dispatch with a malicious version input containing shell metacharacters. For example, if an attacker provides version as 1.0.0"; rm -rf /; echo ", the shell would execute:

VERSION="1.0.0"; rm -rf /; echo ""
gh release create ...

This allows arbitrary command execution on the runner, potentially enabling theft of GITHUB_TOKEN and other repository secrets, code exfiltration, or runner compromise.

The vulnerable code stores the untrusted inputs.version directly into VERSION without escaping, then uses it unquoted in the gh release create command and in string interpolation for the release title. While env.ARTIFACT_DIGEST, env.ARTIFACT_NAME, and env.BUILD_WORKFLOW_REF are safe (they come from environment variables, not user input), the inputs.version variable poses a direct injection risk.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
run: |
set -euo pipefail
VERSION="${{ inputs.version }}"
gh release create "$VERSION" \
"${{ env.ARTIFACT_NAME }}" \
--title "Version $VERSION" \
--notes "## Segment.xcframework ${VERSION}
Binary Swift package release.
### Consumer verification
Verify the build attestation:
\`\`\`
gh attestation verify Segment.xcframework.zip -R ${{ github.repository }} --signer-workflow ${{ env.BUILD_WORKFLOW_REF }}
\`\`\`
### Checksum (for Package.swift binaryTarget)
\`\`\`
${{ env.ARTIFACT_DIGEST }}
\`\`\`"
env:
VERSION: ${{ inputs.version }}
REPOSITORY: ${{ github.repository }}
ARTIFACT_NAME: ${{ env.ARTIFACT_NAME }}
BUILD_WORKFLOW_REF: ${{ env.BUILD_WORKFLOW_REF }}
ARTIFACT_DIGEST: ${{ env.ARTIFACT_DIGEST }}
run: |
set -euo pipefail
gh release create "$VERSION" \
"$ARTIFACT_NAME" \
--title "Version $VERSION" \
--notes "## Segment.xcframework $VERSION
Binary Swift package release.
### Consumer verification
Verify the build attestation:
\`\`\`
gh attestation verify Segment.xcframework.zip -R $REPOSITORY --signer-workflow $BUILD_WORKFLOW_REF
\`\`\`
### Checksum (for Package.swift binaryTarget)
\`\`\`
$ARTIFACT_DIGEST
\`\`\`"
View step-by-step instructions
  1. Move the untrusted workflow input out of the shell script and into the step env: block. For example, set VERSION: ${{ inputs.version }} on the Create GitHub Release step instead of assigning VERSION="${{ inputs.version }}" inside run:.

  2. Remove the direct ${{ inputs.version }} interpolation from the run: script and use the environment variable instead. Keep all shell references quoted, such as "$VERSION".

  3. If you need the repository value inside the release notes text, pass it through env: as well and reference it from the shell, for example REPOSITORY: ${{ github.repository }} and then use "$REPOSITORY" in the command text. This avoids mixing GitHub expression expansion directly into a shell script.

  4. Update the gh release create command so the shell only reads environment variables inside run:. For example, use values like "$VERSION", "$ARTIFACT_NAME", "$BUILD_WORKFLOW_REF", and "$ARTIFACT_DIGEST" in the command arguments and notes body.

  5. Keep the multi-line --notes argument as a shell string that expands only environment variables, not ${{ ... }} expressions. This prevents user-controlled workflow input from being interpreted by the runner before the shell handles quoting.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

Need help with this issue? Consult our appsec team or ask in #help-appsec on Slack.

You can view more details about this finding in the Semgrep AppSec Platform.

Comment on lines +54 to +66
run: |
set -euo pipefail
COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}')
EXPECTED_DIGEST="${{ inputs.artifact-digest }}"

if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "::error::DIGEST MISMATCH — artifact may have been tampered with"
exit 1
fi

echo "Digest verified: $COMPUTED_DIGEST"
echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV"

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Semgrep identified an issue in your code:

User-controlled workflow input inputs.artifact-digest is directly interpolated into a shell script, allowing command injection. An attacker could inject arbitrary commands to steal secrets or compromise the workflow.

More details about this

The run: step directly interpolates ${{ inputs.artifact-digest }} into the shell script without any sanitization. Since inputs.artifact-digest is passed from user input through the workflow input parameter, an attacker could inject arbitrary shell commands into the EXPECTED_DIGEST variable.

Exploit scenario:

  1. An attacker submits a workflow dispatch request with artifact-digest set to something like "; rm -rf /; #
  2. This malicious value gets interpolated directly into the script: EXPECTED_DIGEST="${{ inputs.artifact-digest }}" becomes EXPECTED_DIGEST=""; rm -rf /; #"
  3. The shell interprets the injected commands and executes them with the runner's full permissions
  4. The attacker can now steal secrets from $GITHUB_TOKEN, exfiltrate the repository code, or modify the artifact before publishing

The comparison logic in the conditional doesn't execute unless the attacker's injected code is valid shell syntax, but they can still inject commands before it completes or use shell metacharacters to break out of the string and execute arbitrary code.

To resolve this comment:

✨ Commit fix suggestion

Suggested change
run: |
set -euo pipefail
COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}')
EXPECTED_DIGEST="${{ inputs.artifact-digest }}"
if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "::error::DIGEST MISMATCH — artifact may have been tampered with"
exit 1
fi
echo "Digest verified: $COMPUTED_DIGEST"
echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV"
env:
EXPECTED_DIGEST: ${{ inputs.artifact-digest }}
run: |
set -euo pipefail
COMPUTED_DIGEST=$(shasum -a 256 "${{ env.ARTIFACT_NAME }}" | awk '{print $1}')
if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then
echo "::error::DIGEST MISMATCH — artifact may have been tampered with"
exit 1
fi
echo "Digest verified: $COMPUTED_DIGEST"
echo "ARTIFACT_DIGEST=$COMPUTED_DIGEST" >> "$GITHUB_ENV"
View step-by-step instructions
  1. Move the untrusted workflow input out of the shell script and into the step env: block.
    Add something like env: EXPECTED_DIGEST: ${{ inputs.artifact-digest }} on the Verify artifact digest step.

  2. Stop interpolating ${{ inputs.artifact-digest }} inside run:.
    Replace EXPECTED_DIGEST="${{ inputs.artifact-digest }}" with EXPECTED_DIGEST="$EXPECTED_DIGEST" or compare directly against "$EXPECTED_DIGEST" in the if statement.

  3. Use the environment variable with double quotes everywhere it is read in the shell.
    Keep the comparison in the form if [ "$COMPUTED_DIGEST" != "$EXPECTED_DIGEST" ]; then ... fi so the value is treated as data, not shell syntax.

  4. Apply the same pattern to other run: steps that interpolate ${{ ... }} values from inputs or github context.
    For example, move values such as ${{ inputs.version }}, ${{ github.repository }}, and ${{ github.sha }} into step-level env: entries, then reference them as "$VERSION", "$GITHUB_REPOSITORY", or "$GITHUB_SHA" inside the script.

💬 Ignore this finding

Reply with Semgrep commands to ignore this finding.

  • /fp <comment> for false positive
  • /ar <comment> for acceptable risk
  • /other <comment> for all other reasons

Alternatively, triage in Semgrep AppSec Platform to ignore the finding created by run-shell-injection.

Need help with this issue? Consult our appsec team or ask in #help-appsec on Slack.

You can view more details about this finding in the Semgrep AppSec Platform.

wenxi-zeng and others added 5 commits July 2, 2026 14:23
SPM/git on Linux wasn't reading ~/.netrc for auth. Use git credential
store with the token embedded in ~/.git-credentials instead.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Use git url.insteadOf to embed token in all Artifactory URLs (more
  reliable than credential store or .netrc)
- Add token verification step to catch OIDC issues early
- Limit push trigger to poc/** branches to avoid double-trigger

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The Swift virtual repo expects a bearer credential per the docs.
Use git http.extraHeader to pass Authorization: Bearer instead of
basic-auth credentials.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant