diff --git a/.github/filters.yml b/.github/filters.yml new file mode 100644 index 0000000..62d34e8 --- /dev/null +++ b/.github/filters.yml @@ -0,0 +1,31 @@ +php84-debian12: + - 'php84-debian12/**' + - 'Dockerfile' + +php84-debian11: + - 'php84-debian11/**' + - 'Dockerfile' + +php83-debian12: + - 'php83-debian12/**' + - 'Dockerfile' + +php83-debian11: + - 'php83-debian11/**' + - 'Dockerfile' + +php82-debian12: + - 'php82-debian12/**' + - 'Dockerfile' + +php82-debian11: + - 'php82-debian11/**' + - 'Dockerfile' + +php81-debian12: + - 'php81-debian12/**' + - 'Dockerfile' + +php81-debian11: + - 'php81-debian11/**' + - 'Dockerfile' diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index ab38836..2cb2e33 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,46 +7,45 @@ jobs: changes: name: Detect Changes runs-on: ubuntu-latest - permissions: - pull-requests: read outputs: variants: ${{ steps.filter.outputs.changes }} steps: - name: Checkout code uses: actions/checkout@v4 + with: + fetch-depth: ${{ startsWith(github.ref, 'refs/tags/v') && 0 || 1 }} + + - name: Retrieve the previous release version + if: startsWith(github.ref, 'refs/tags/v') + id: previous_release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Attempt to get the tag of the second-to-last release + PREVIOUS_TAG=$(gh release list --limit 2 --json tagName | jq -r '. | select(length > 1) | .[1].tagName') + + if [[ -z "$PREVIOUS_TAG" ]]; then + echo "No previous release found. Falling back to the first commit." + REF=$(git rev-list --max-parents=0 HEAD) + else + echo "Found previous release: $PREVIOUS_TAG" + REF=$PREVIOUS_TAG + fi + + echo "The reference point is: $REF" + echo "ref=$REF" >> $GITHUB_OUTPUT + - uses: dorny/paths-filter@v3 id: filter with: - filters: | - php84-debian12: - - 'php84-debian12/**' - - 'Dockerfile' - php84-debian11: - - 'php84-debian11/**' - - 'Dockerfile' - php83-debian12: - - 'php83-debian12/**' - - 'Dockerfile' - php83-debian11: - - 'php83-debian11/**' - - 'Dockerfile' - php82-debian12: - - 'php82-debian12/**' - - 'Dockerfile' - php82-debian11: - - 'php82-debian11/**' - - 'Dockerfile' - php81-debian12: - - 'php81-debian12/**' - - 'Dockerfile' - php81-debian11: - - 'php81-debian11/**' - - 'Dockerfile' + filters: .github/filters.yml + base: ${{ startsWith(github.ref, 'refs/tags/v') && steps.previous_release.outputs.ref || '' }} build: needs: changes name: Build ${{ matrix.variant }} image runs-on: ubuntu-latest + if: needs.changes.outputs.variants != '[]' concurrency: group: build-${{ matrix.variant }} cancel-in-progress: true @@ -54,6 +53,9 @@ jobs: fail-fast: false matrix: variant: ${{ fromJson(needs.changes.outputs.variants) }} + permissions: + contents: read + packages: write steps: - name: Checkout code uses: actions/checkout@v4 @@ -94,3 +96,15 @@ jobs: - name: Test run: make test-setup test-${{ matrix.variant }} + + - name: Login to GitHub Container Registry + if: startsWith(github.ref, 'refs/tags/v') + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Push to GitHub Container Registry + if: startsWith(github.ref, 'refs/tags/v') + run: make push-${{ matrix.variant }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..68c422e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,107 @@ +name: Release + +on: + release: + types: [published] + +jobs: + changes: + name: Detect changes since last release + runs-on: ubuntu-latest + outputs: + variants: ${{ steps.filter.outputs.changes }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Retrieve the previous release version + id: previous_release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Attempt to get the tag of the second-to-last release + PREVIOUS_TAG=$(gh release list --limit 2 --json tagName | jq -r '. | select(length > 1) | .[1].tagName') + + if [[ -z "$PREVIOUS_TAG" ]]; then + echo "No previous release found. Falling back to the first commit." + REF=$(git rev-list --max-parents=0 HEAD) + else + echo "Found previous release: $PREVIOUS_TAG" + REF=$PREVIOUS_TAG + fi + + echo "The reference point is: $REF" + echo "ref=$REF" >> $GITHUB_OUTPUT + + - name: Detect changed variants + uses: dorny/paths-filter@v3 + id: filter + with: + filters: .github/filters.yml + base: ${{ steps.previous_release.outputs.ref }} + + release: + name: Release ${{ matrix.variant }} + needs: changes + runs-on: ubuntu-latest + if: needs.changes.outputs.variants != '[]' + permissions: + contents: write + strategy: + fail-fast: false + matrix: + variant: ${{ fromJson(needs.changes.outputs.variants) }} + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.4' + + - name: Get composer cache directory + id: composer-cache + working-directory: ./publish + run: echo "dir=$(composer config cache-files-dir)" >> $GITHUB_OUTPUT + + - name: Cache dependencies + uses: actions/cache@v4 + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-composer-${{ hashFiles('publish/composer.lock') }} + restore-keys: ${{ runner.os }}-composer- + + - name: Install dependencies + working-directory: ./publish + run: composer install --prefer-dist + + - name: Pull image from registry + run: | + TAG=$(echo "${{ matrix.variant }}" | sed 's/^php//') + docker pull ghcr.io/${{ github.repository_owner }}/php:$TAG + + - name: Export artifact + env: + DOCKER_REGISTRY: ghcr.io + DOCKER_IMAGE: ${{ github.repository_owner }}/php + run: make export/${{ matrix.variant }} + + - name: Upload release asset + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + gh release upload ${{ github.event.release.tag_name }} \ + ./export/${{ matrix.variant }}.zip \ + --clobber + + - name: Publish to Alibaba Cloud + env: + OSS_BUCKET: ${{ secrets.OSS_BUCKET }} + ACS_ACCESS_KEY_ID: ${{ secrets.ACS_ACCESS_KEY_ID }} + ACS_ACCESS_KEY_SECRET: ${{ secrets.ACS_ACCESS_KEY_SECRET }} + run: | + RELEASE="${{ github.event.release.tag_name }}" \ + make publish-${{ matrix.variant }} diff --git a/Makefile b/Makefile index 38752a0..78bf82d 100644 --- a/Makefile +++ b/Makefile @@ -1,5 +1,7 @@ OBJECTS = php81 php82 php83 php84 VARIANTS = $(addsuffix -debian11,$(OBJECTS)) $(addsuffix -debian12,$(OBJECTS)) +DOCKER_REGISTRY ?= ghcr.io +DOCKER_IMAGE ?= dew-serverless/php DOCKER_BUILD_EXTRA ?= .PHONY: build export publish clean @@ -8,8 +10,7 @@ build-%: BUILD_ARGS="$(shell awk '/^[a-zA-Z0-9]+ *=/ { printf "--build-arg %s_VERSION=%s ", toupper($$1), $$3 }' "$*/dependencies.ini" | xargs)" && \ docker buildx build $$BUILD_ARGS $(DOCKER_BUILD_EXTRA) \ --load \ - -t dew/$* \ - -t ghcr.io/dew-serverless/php:$(subst php,,$*) \ + -t $(DOCKER_BUILD_EXTRA)/$(DOCKER_IMAGE):$(subst php,,$*) \ . build: $(addprefix build-,$(VARIANTS)) @@ -25,18 +26,20 @@ test-%: test: test-setup $(addprefix test-,$(VARIANTS)) export/%: - CID=$$(docker create dew/$*) && docker cp $$CID:/opt ./export/$* && docker rm $$CID - cd export/$*; zip -r ../$*.zip . + CID=$$(docker create $(DOCKER_REGISTRY)/$(DOCKER_IMAGE):$(subst php,,$*)) && \ + docker cp $$CID:/opt ./export/$* && \ + docker rm $$CID && \ + cd export/$* && zip -r ../$*.zip . export: $(addprefix export/,$(VARIANTS)) publish-%: export/% - php publish/publish.php $* + php publish/publish.php $* $(RELEASE) publish: $(addprefix publish-,$(VARIANTS)) push-%: build-% - docker push ghcr.io/dew-serverless/php:$(subst php,,$*) + docker push $(DOCKER_REGISTRY)/$(DOCKER_IMAGE):$(subst php,,$*) push: $(addprefix push-,$(VARIANTS)) diff --git a/publish/Crc64.php b/publish/Crc64.php index c1f6cbd..00946ad 100644 --- a/publish/Crc64.php +++ b/publish/Crc64.php @@ -156,7 +156,7 @@ final class Crc64 0xD80C07CD << 32 | 0x676F8394, 0x9AFCE626 << 32 | 0xCE85B507, ]; - public static function make(string $value): int + public static function make(string $value): string { // Final XOR: 0xFFFFFFFFFFFFFFFF $finalXor = (1 << 64) - 1; @@ -177,7 +177,7 @@ public static function make(string $value): int $checksum = static::reflect64($checksum); - return $checksum ^ $finalXor; + return sprintf('%u', $checksum ^ $finalXor); } private static function reflect8(int $value): int diff --git a/publish/composer.json b/publish/composer.json index f015712..b4da272 100644 --- a/publish/composer.json +++ b/publish/composer.json @@ -1,6 +1,6 @@ { "require": { - "dew-serverless/acs-sdk-php": "^0.7.0", + "dew-serverless/acs-sdk-php": "^0.9.0", "php-http/guzzle7-adapter": "^1.1", "guzzlehttp/guzzle": "^7.0" }, diff --git a/publish/composer.lock b/publish/composer.lock index ccf530b..2808604 100644 --- a/publish/composer.lock +++ b/publish/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "52f2a9d391668a15e3d3df43f254724b", + "content-hash": "ac3e80ea54ba793f9a6b28a59083dda0", "packages": [ { "name": "clue/stream-filter", @@ -74,16 +74,16 @@ }, { "name": "dew-serverless/acs-sdk-php", - "version": "v0.7.0+20241130", + "version": "v0.9.0+20250818", "source": { "type": "git", "url": "https://github.com/dew-serverless/acs-sdk-php.git", - "reference": "3572e254b6b6f1021ed8c060df4262bad288393b" + "reference": "6f27f9a531fe191312072df483c88315faeb3a59" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/dew-serverless/acs-sdk-php/zipball/3572e254b6b6f1021ed8c060df4262bad288393b", - "reference": "3572e254b6b6f1021ed8c060df4262bad288393b", + "url": "https://api.github.com/repos/dew-serverless/acs-sdk-php/zipball/6f27f9a531fe191312072df483c88315faeb3a59", + "reference": "6f27f9a531fe191312072df483c88315faeb3a59", "shasum": "" }, "require": { @@ -130,9 +130,9 @@ "description": "Unofficial Alibaba Cloud PHP SDK.", "support": { "issues": "https://github.com/dew-serverless/acs-sdk-php/issues", - "source": "https://github.com/dew-serverless/acs-sdk-php/tree/v0.7.0+20241130" + "source": "https://github.com/dew-serverless/acs-sdk-php/tree/v0.9.0+20250818" }, - "time": "2024-11-30T13:52:28+00:00" + "time": "2025-08-18T04:02:07+00:00" }, { "name": "guzzlehttp/guzzle", diff --git a/publish/publish.php b/publish/publish.php index 7780736..a60cbdf 100644 --- a/publish/publish.php +++ b/publish/publish.php @@ -10,25 +10,27 @@ // See: https://www.alibabacloud.com/help/zh/functioncompute/fc-3-0/product-overview/supported-regions // See: https://www.alibabacloud.com/help/en/functioncompute/fc-3-0/product-overview/supported-regions $regions = [ - 'ap-northeast-1', - 'ap-northeast-2', + 'cn-hangzhou', + 'cn-shanghai', + 'cn-qingdao', + 'cn-beijing', + 'cn-zhangjiakou', + 'cn-huhehaote', + 'cn-wulanchabu', + 'cn-shenzhen', + 'cn-chengdu', + 'cn-hongkong', 'ap-southeast-1', 'ap-southeast-3', 'ap-southeast-5', 'ap-southeast-7', - 'cn-beijing', - 'cn-chengdu', - 'cn-hangzhou', - 'cn-hongkong', - 'cn-huhehaote', - 'cn-qingdao', - 'cn-shanghai', - 'cn-shenzhen', - 'cn-zhangjiakou', + 'ap-northeast-1', + 'ap-northeast-2', 'eu-central-1', 'eu-west-1', 'us-east-1', 'us-west-1', + 'me-central-1', ]; $bucket = getenv('OSS_BUCKET'); @@ -36,9 +38,10 @@ $accessKeyId = getenv('ACS_ACCESS_KEY_ID'); $accessKeySecret = getenv('ACS_ACCESS_KEY_SECRET'); -$runtime = $argv[1] ?? null; -$runtimePath = __DIR__.'/../export/'.$runtime.'.zip'; -$objectName = $runtime.'.zip'; +$variant = $argv[1] ?? ''; +$release = $argv[2] ?? ''; +$filename = __DIR__.'/../export/'.$variant.'.zip'; +$objectName = sprintf('%s-%s.zip', $variant, normalizeRelease($release)); if (! $bucket) { fatal('Expect the base name of OSS bucket'); @@ -48,12 +51,16 @@ fatal('Expect ACS credentials'); } -if (! $runtime) { - fatal('Expect runtime name, e.g. php84-debian11'); +if ($variant === '') { + fatal('Expect a variant name, e.g. php84-debian11'); +} + +if ($release === '') { + fatal('Expect a release version, e.g. v2025.1'); } -if (! file_exists($runtimePath)) { - fatal('The runtime layer package is missing'); +if (! file_exists($filename)) { + fatal('The runtime package is missing'); } function fatal(string $message): void @@ -68,6 +75,20 @@ function step(string $message): void printf('[-] %s'.PHP_EOL, $message); } +function normalizeRelease(string $release): string +{ + // Ensure the release has the leading 'v' + $normalized = str_starts_with($release, 'v') ? $release : 'v'.$release; + + // Then, we get rid of the leading 'v' + $normalized = substr($normalized, 1); + + // Replace the dot with an underscore + $normalized = str_replace('.', '_', $normalized); + + return $normalized; +} + function createOssClient(string $key, string $secret, string $region): OssClient { return new OssClient([ @@ -76,6 +97,7 @@ function createOssClient(string $key, string $secret, string $region): OssClient 'secret' => $secret, ], 'region' => $region, + 'endpoint' => sprintf('oss-%s.aliyuncs.com', $region), ]); } @@ -91,13 +113,13 @@ function createFcClient(string $key, string $secret, string $region): FcClient ]); } -function fileExists(OssClient $client, string $bucket, string $object, string $filename): bool +function fileExists(OssClient $client, string $bucket, string $object, string $filename, string $md5): bool { try { $client->headObject([ 'bucket' => $bucket, 'key' => $object, - 'If-Match' => strtoupper(md5_file($filename)), + 'If-Match' => strtoupper($md5), ]); return true; @@ -106,26 +128,29 @@ function fileExists(OssClient $client, string $bucket, string $object, string $f } } -function fileUpload(OssClient $client, string $bucket, string $object, string $filename): void +function fileUpload(OssClient $client, string $bucket, string $object, string $filename, string $md5): void { $client->putObject([ 'bucket' => $bucket, 'key' => $object, 'body' => file_get_contents($filename), + '@headers' => [ + 'Content-MD5' => $md5, + ], ]); } -function fileChecksum(string $filename): int +function fileChecksum(string $filename): string { $contents = file_get_contents($filename); return Crc64::make($contents); } -function layerExists(FcClient $client, string $runtime, string $checksum): bool +function layerExists(FcClient $client, string $variant, string $checksum): bool { $data = $client->listLayers([ - 'prefix' => $runtime, + 'prefix' => $variant, 'limit' => 1, 'official' => 'false', // A type of string by API definition ])->getDecodedData(); @@ -141,27 +166,28 @@ function layerExists(FcClient $client, string $runtime, string $checksum): bool return true; } -function layerPublish(FcClient $client, string $runtime, string $bucket, string $object): void +function layerPublish(FcClient $client, string $variant, string $bucket, string $object, string $checksum): void { $client->createLayerVersion([ - 'layerName' => $runtime, + 'layerName' => $variant, 'body' => [ 'code' => [ 'ossBucketName' => $bucket, 'ossObjectName' => $object, + 'checksum' => $checksum, ], 'compatibleRuntime' => [ - getRuntimeFromLayerName($runtime), + getRuntimeFromLayerName($variant), ], 'license' => 'MIT', ], ]); } -function layerEnsureIsPublic(FcClient $client, string $runtime): void +function layerEnsureIsPublic(FcClient $client, string $variant): void { $client->putLayerACL([ - 'layerName' => $runtime, + 'layerName' => $variant, // Allowed values: // '0': private (default) @@ -173,15 +199,18 @@ function layerEnsureIsPublic(FcClient $client, string $runtime): void function getRuntimeFromLayerName(string $layerName): string { return match (true) { + str_ends_with($layerName, '-debian12') => 'custom.debian12', str_ends_with($layerName, '-debian11') => 'custom.debian11', str_ends_with($layerName, '-debian10') => 'custom.debian10', default => 'custom', }; } -step("Process {$runtime} runtime"); +step("Process {$variant} runtime"); -$checksum = fileChecksum($runtimePath); +$crc64 = fileChecksum($filename); +$md5 = md5_file($filename); +$md5Base64 = base64_encode(md5_file($filename, true)); foreach ($regions as $region) { $bucketName = $bucket.'-'.$region; @@ -189,11 +218,11 @@ function getRuntimeFromLayerName(string $layerName): string $oss = createOssClient($accessKeyId, $accessKeySecret, $region); $fc = createFcClient($accessKeyId, $accessKeySecret, $region); - if (fileExists($oss, $bucketName, $objectName, $runtimePath)) { + if (fileExists($oss, $bucketName, $objectName, $filename, $md5)) { step("Upload layer to region {$region} (exists)"); } else { step("Upload layer to region {$region}"); - fileUpload($oss, $bucketName, $objectName, $runtimePath); + fileUpload($oss, $bucketName, $objectName, $filename, $md5Base64); } // Instead of partially uploading the layer package one step at a time, @@ -201,15 +230,15 @@ function getRuntimeFromLayerName(string $layerName): string // is about 50MiB, we force garbage collection and free up memory. gc_collect_cycles(); - if (layerExists($fc, $runtime, $checksum)) { + if (layerExists($fc, $variant, $crc64)) { step("Release layer to region {$region} (exists)"); } else { step("Release layer to region {$region}"); - layerPublish($fc, $runtime, $bucketName, $objectName); + layerPublish($fc, $variant, $bucketName, $objectName, $crc64); } step("Ensure layer is public in {$region}"); - layerEnsureIsPublic($fc, $runtime); + layerEnsureIsPublic($fc, $variant); } step('Publish is done');