Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@
## Checklist

- [ ] No secrets committed
- [ ] Built wrapper: `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/build.sh`
- [ ] Collected SDK xcframeworks: `bash src/Kapusch.FacebookApisForiOSComponents/Native/iOS/collect-facebook-xcframeworks.sh`
- [ ] Packed NuGet: `dotnet pack src/Kapusch.FacebookApisForiOSComponents/Kapusch.FacebookApisForiOSComponents.csproj -c Release -o artifacts/nuget`
- [ ] Built wrapper: `bash src/Kapusch.StoreKit2ApisForiOSComponents/Native/iOS/build.sh`
- [ ] Built package project and sample
- [ ] Packed and validated the NuGet with `scripts/validate-nupkg.py`
- [ ] Parsed modified workflows
- [ ] Updated docs if behavior/integration changed
46 changes: 14 additions & 32 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,29 @@ name: ci

on:
pull_request:
push:
branches:
- master
workflow_dispatch:

permissions:
contents: read

jobs:
build_pack_ios:
runs-on: macos-latest
runs-on: macos-15
steps:
- uses: actions/checkout@v4

- name: Select Xcode 26.3
run: sudo xcode-select --switch /Applications/Xcode_26.3.app/Contents/Developer

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
global-json-file: global.json

- name: Install iOS workload
run: dotnet workload install ios
run: dotnet workload install ios --version 10.0.202

- name: Build iOS wrapper
run: |
Expand All @@ -28,40 +34,16 @@ jobs:
run: |
dotnet build src/Kapusch.StoreKit2ApisForiOSComponents/Kapusch.StoreKit2ApisForiOSComponents.csproj -c Release

- name: Build sample
run: |
dotnet build samples/Kapusch.StoreKit2.iOS.Sample/Kapusch.StoreKit2.iOS.Sample.csproj -c Release

- name: Pack
run: |
dotnet pack src/Kapusch.StoreKit2ApisForiOSComponents/Kapusch.StoreKit2ApisForiOSComponents.csproj -c Release -o artifacts/nuget

- name: Validate nupkg layout
run: |
python3 - <<'PY'
import glob, sys, zipfile

nupkgs = glob.glob("artifacts/nuget/*.nupkg")
if not nupkgs:
print("ERROR: no .nupkg found under artifacts/nuget/")
sys.exit(1)

nupkg = sorted(nupkgs)[-1]
print(f"Validating {nupkg}")

z = zipfile.ZipFile(nupkg)
names = set(z.namelist())

required = [
"buildTransitive/Kapusch.StoreKit2.iOS.targets",
"kstorekit2.xcframework/Info.plist",
]

missing = [p for p in required if p not in names]
if missing:
print("ERROR: missing required paths in nupkg:")
for p in missing:
print(f" - {p}")
sys.exit(1)

print("OK: nupkg layout looks correct.")
PY
run: python3 scripts/validate-nupkg.py artifacts/nuget/*.nupkg

- uses: actions/upload-artifact@v4
with:
Expand Down
221 changes: 160 additions & 61 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,11 @@ on:
workflow_dispatch:
inputs:
manual_version:
description: 'Force a specific version for NuGet.org publish (e.g., 1.0.0 or 1.0.0-rc.1). If empty, defaults to auto-generated preview for GitHub Packages.'
description: 'Publish this immutable version to NuGet.org (for example 1.1.0 or 1.1.0-rc.1). Must be dispatched from master. Empty publishes a preview to GitHub Packages.'
required: false
type: string
source_ref:
description: 'Optional historical tag, branch, or commit to package. It must resolve to a commit reachable from origin/master.'
required: false
type: string
push:
Expand All @@ -17,7 +21,7 @@ permissions:

jobs:
pack_and_publish:
runs-on: macos-latest
runs-on: macos-15
permissions:
contents: read
packages: write
Expand All @@ -27,22 +31,79 @@ jobs:
with:
fetch-depth: 0

- name: Select Xcode 26.3
run: sudo xcode-select --switch /Applications/Xcode_26.3.app/Contents/Developer

- name: Resolve and authorize package source
id: source
shell: bash
env:
MANUAL_VERSION: ${{ github.event.inputs.manual_version }}
SOURCE_REF: ${{ github.event.inputs.source_ref }}
run: |
git fetch --no-tags origin '+refs/heads/*:refs/remotes/origin/*'
git fetch --tags origin

if [[ "${{ github.event_name }}" == "workflow_dispatch" && -n "$SOURCE_REF" ]]; then
if RESOLVED_SHA=$(git rev-parse --verify --end-of-options "${SOURCE_REF}^{commit}" 2>/dev/null); then
:
else
BRANCH_NAME="${SOURCE_REF#refs/heads/}"
RESOLVED_SHA=$(git rev-parse --verify --end-of-options "origin/${BRANCH_NAME}^{commit}")
fi
git checkout --detach "$RESOLVED_SHA"
else
RESOLVED_SHA=$(git rev-parse --verify HEAD^{commit})
fi

if [[ -n "$MANUAL_VERSION" && "${{ github.ref }}" != "refs/heads/master" ]]; then
echo "ERROR: a manual NuGet.org version publish must be dispatched from master."
exit 1
fi

REQUIRE_MASTER_REACHABILITY="false"
if [[ "${{ github.ref }}" == refs/tags/v* || -n "$MANUAL_VERSION" || -n "$SOURCE_REF" ]]; then
REQUIRE_MASTER_REACHABILITY="true"
fi

if [[ "$REQUIRE_MASTER_REACHABILITY" == "true" ]] && ! git merge-base --is-ancestor "$RESOLVED_SHA" origin/master; then
echo "ERROR: package source $RESOLVED_SHA is not reachable from origin/master."
exit 1
fi

if [[ "${{ github.ref }}" == refs/tags/v* ]]; then
TAG_SHA=$(git rev-list -n 1 "${{ github.ref }}")
if [[ "$TAG_SHA" != "$RESOLVED_SHA" ]]; then
echo "ERROR: tag target $TAG_SHA does not match checked-out source $RESOLVED_SHA."
exit 1
fi
fi

echo "sha=${RESOLVED_SHA}" >> "$GITHUB_OUTPUT"
echo "Package source input: ${SOURCE_REF:-<trigger ref>}"
echo "Package source SHA: $RESOLVED_SHA"
echo "origin/master SHA: $(git rev-parse origin/master)"

- name: Determine package version
id: version
shell: bash
run: |
REF="${{ github.ref }}"
EVENT="${{ github.event_name }}"
MANUAL_VERSION="${{ github.event.inputs.manual_version }}"
CSPROJ="src/Kapusch.StoreKit2ApisForiOSComponents/Kapusch.StoreKit2ApisForiOSComponents.csproj"
BASE_VERSION=$(grep -oE '<Version>[^<]+' "$CSPROJ" | head -n 1 | sed 's/<Version>//' || echo "1.0.0")

if [[ -n "$MANUAL_VERSION" ]]; then
VERSION="$MANUAL_VERSION"
PUBLISH_TARGET="nuget"
echo "Manual version provided ($VERSION). Triggering NuGet.org publish."
echo "Manual version provided ($VERSION)."
elif [[ "$REF" =~ ^refs/tags/v ]]; then
VERSION="${REF#refs/tags/v}"
TAG="${REF#refs/tags/}"
if [[ ! "$TAG" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
echo "ERROR: release tags must be vX.Y.Z or vX.Y.Z-rc.N."
exit 1
fi
VERSION="${TAG#v}"
PUBLISH_TARGET="nuget"
else
COMMIT_SHORT=$(git rev-parse --short HEAD)
Expand All @@ -51,43 +112,68 @@ jobs:
PUBLISH_TARGET="github"
fi

if [[ "$PUBLISH_TARGET" == "nuget" ]]; then
git fetch origin +refs/heads/*:refs/remotes/origin/*
TAGGED_SHA="${{ github.sha }}"
CONTAINING_BRANCHES=$(git for-each-ref --format='%(refname:short)' refs/remotes/origin --contains "$TAGGED_SHA")

ON_MASTER="false"
ON_RELEASE="false"
while IFS= read -r branch; do
[[ "$branch" == "origin/master" ]] && ON_MASTER="true"
[[ "$branch" == origin/release/* ]] && ON_RELEASE="true"
done <<< "$CONTAINING_BRANCHES"

if [[ "$VERSION" == *-* ]]; then
if [[ "$ON_RELEASE" != "true" ]]; then
echo "ERROR: Pre-release version $VERSION must be on origin/release/*"
exit 1
fi
else
if [[ "$ON_MASTER" != "true" ]]; then
echo "ERROR: Stable version $VERSION must be on origin/master"
exit 1
fi
fi
if [[ "$PUBLISH_TARGET" == "nuget" && ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+(-rc\.[0-9]+)?$ ]]; then
echo "ERROR: NuGet.org versions must be X.Y.Z or X.Y.Z-rc.N."
exit 1
fi

echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "publish_target=${PUBLISH_TARGET}" >> "$GITHUB_OUTPUT"
echo "Package version: ${VERSION}"
echo "Publish target: ${PUBLISH_TARGET}"

- name: Protect existing tag provenance
if: steps.version.outputs.publish_target == 'nuget'
shell: bash
env:
VERSION: ${{ steps.version.outputs.version }}
SOURCE_SHA: ${{ steps.source.outputs.sha }}
run: |
TAG_REF="refs/tags/v${VERSION}"
if git show-ref --verify --quiet "$TAG_REF"; then
TAG_SHA=$(git rev-list -n 1 "$TAG_REF")
if [[ "$TAG_SHA" != "$SOURCE_SHA" ]]; then
echo "ERROR: existing tag v${VERSION} targets $TAG_SHA, not package source $SOURCE_SHA."
echo "Existing tags are immutable and will not be moved."
exit 1
fi
echo "Existing tag v${VERSION} already records package source $SOURCE_SHA; it remains unchanged."
else
echo "No existing v${VERSION} tag; no tag will be created or moved by this workflow."
fi

- name: Verify NuGet.org version is unused
if: steps.version.outputs.publish_target == 'nuget'
env:
PACKAGE_VERSION: ${{ steps.version.outputs.version }}
run: |
python3 - <<'PY'
import json
import os
import sys
import urllib.request

version = os.environ["PACKAGE_VERSION"].lower()
url = "https://api.nuget.org/v3-flatcontainer/kapusch.storekit2.ios/index.json"
print(f"Checking official NuGet index: {url}")
with urllib.request.urlopen(url) as response:
versions = {item.lower() for item in json.load(response).get("versions", [])}

if version in versions:
print(f"ERROR: Kapusch.StoreKit2.iOS {version} already exists on NuGet.org.")
print("NuGet versions are immutable; refusing to attempt a republish.")
sys.exit(1)

print(f"OK: Kapusch.StoreKit2.iOS {version} is not present on NuGet.org.")
PY

- uses: actions/setup-dotnet@v4
with:
dotnet-version: "10.0.x"
global-json-file: global.json

- name: Install iOS workload
run: |
dotnet workload install ios
dotnet workload install ios --version 10.0.202

- name: Cache NuGet
uses: actions/cache@v4
Expand All @@ -104,43 +190,57 @@ jobs:
dotnet pack src/Kapusch.StoreKit2ApisForiOSComponents/Kapusch.StoreKit2ApisForiOSComponents.csproj \
-c Release \
-o artifacts/nuget \
/p:PackageVersion="${{ steps.version.outputs.version }}"
/p:PackageVersion="${{ steps.version.outputs.version }}" \
/p:RepositoryCommit="${{ steps.source.outputs.sha }}" \
/p:ContinuousIntegrationBuild=true

- name: Validate nupkg layout
run: python3 scripts/validate-nupkg.py artifacts/nuget/*.nupkg

- name: Record package provenance
env:
PACKAGE_VERSION: ${{ steps.version.outputs.version }}
PUBLISH_TARGET: ${{ steps.version.outputs.publish_target }}
SOURCE_SHA: ${{ steps.source.outputs.sha }}
SOURCE_REF_INPUT: ${{ github.event.inputs.source_ref }}
TRIGGER_REF: ${{ github.ref }}
WORKFLOW_RUN_URL: https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
python3 - <<'PY'
import glob, sys, zipfile

nupkgs = glob.glob("artifacts/nuget/*.nupkg")
if not nupkgs:
print("ERROR: no .nupkg found under artifacts/nuget/")
sys.exit(1)

nupkg = sorted(nupkgs)[-1]
print(f"Validating {nupkg}")
import glob
import hashlib
import json
import os
from pathlib import Path

z = zipfile.ZipFile(nupkg)
names = set(z.namelist())
packages = glob.glob("artifacts/nuget/*.nupkg")
if len(packages) != 1:
raise SystemExit(f"ERROR: expected one nupkg, found {len(packages)}")

required = [
"buildTransitive/Kapusch.StoreKit2.iOS.targets",
"kstorekit2.xcframework/Info.plist",
]

missing = [p for p in required if p not in names]
if missing:
print("ERROR: missing required paths in nupkg:")
for p in missing:
print(f" - {p}")
sys.exit(1)

if "Info.plist" in names:
print("ERROR: wrapper appears flattened (found 'Info.plist' at package root).")
sys.exit(1)

print("OK: nupkg layout looks correct.")
package_path = Path(packages[0])
provenance = {
"package": "Kapusch.StoreKit2.iOS",
"version": os.environ["PACKAGE_VERSION"],
"source_sha": os.environ["SOURCE_SHA"],
"trigger_ref": os.environ["TRIGGER_REF"],
"source_ref_input": os.environ.get("SOURCE_REF_INPUT", ""),
"publish_target": os.environ["PUBLISH_TARGET"],
"run_url": os.environ["WORKFLOW_RUN_URL"],
"nupkg": package_path.name,
"nupkg_sha256": hashlib.sha256(package_path.read_bytes()).hexdigest(),
}
output = Path("artifacts/package-provenance.json")
output.write_text(json.dumps(provenance, indent=2, sort_keys=True) + "\n")
print(output.read_text())
PY

- uses: actions/upload-artifact@v4
with:
name: package-${{ steps.version.outputs.version }}-${{ steps.source.outputs.sha }}
path: |
artifacts/nuget/*.nupkg
artifacts/package-provenance.json

- name: Push to NuGet.org
if: steps.version.outputs.publish_target == 'nuget'
uses: NuGet/login@v1
Expand All @@ -153,8 +253,7 @@ jobs:
run: |
dotnet nuget push artifacts/nuget/*.nupkg \
--api-key "${{ steps.nuget_login.outputs.NUGET_API_KEY }}" \
--source "https://api.nuget.org/v3/index.json" \
--skip-duplicate
--source "https://api.nuget.org/v3/index.json"

- name: Publish to GitHub Packages
if: steps.version.outputs.publish_target == 'github'
Expand Down
Loading
Loading