mirror of
https://github.com/affaan-m/ECC.git
synced 2026-10-14 20:25:26 +02:00
pi/core and the bundled blueprint skill now refer to the project as ECC only; the generated README no longer expands the name. Profile contents, package name and paths are unchanged (123 skills, 24 commands). The release workflows poll npm for up to five minutes after publish before verifying the artifact. In 2.2.2 the check gave up after about 30 seconds while npm was still processing, failing the run before latest was promoted and the GitHub Release was created. Version metadata synced to 2.2.3 with CHANGELOG, translated release headings and release notes; skill-triggers registryDigest updated for the blueprint text.
296 lines
14 KiB
YAML
296 lines
14 KiB
YAML
name: Release
|
|
|
|
on:
|
|
push:
|
|
tags: ['v*']
|
|
|
|
permissions:
|
|
actions: read
|
|
checks: read
|
|
contents: read
|
|
|
|
jobs:
|
|
verify:
|
|
name: Verify Release
|
|
runs-on: ubuntu-latest
|
|
outputs:
|
|
release_sha: ${{ steps.release_gate.outputs.release_sha }}
|
|
tag_object_sha: ${{ steps.release_gate.outputs.tag_object_sha }}
|
|
already_published: ${{ steps.npm_publish_state.outputs.already_published }}
|
|
dist_tag: ${{ steps.npm_publish_state.outputs.dist_tag }}
|
|
publish_tag: ${{ steps.npm_publish_state.outputs.publish_tag }}
|
|
package_name: ${{ steps.npm_publish_state.outputs.package_name }}
|
|
package_version: ${{ steps.npm_publish_state.outputs.package_version }}
|
|
package_file: ${{ steps.pack.outputs.package_file }}
|
|
package_sha256: ${{ steps.pack.outputs.package_sha256 }}
|
|
|
|
steps:
|
|
- name: Checkout
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
fetch-depth: 0
|
|
persist-credentials: false
|
|
|
|
- name: Require the release commit to equal origin main
|
|
run: |
|
|
git fetch origin main --no-tags
|
|
RELEASE_COMMIT=$(git rev-parse HEAD)
|
|
MAIN_COMMIT=$(git rev-parse origin/main)
|
|
if [ "$RELEASE_COMMIT" != "$MAIN_COMMIT" ]; then
|
|
echo "::error::The release commit must equal origin/main exactly"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: '20.x'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
|
|
- name: Verify signed tag and exact-SHA CI gates
|
|
id: release_gate
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
RELEASE_TAG: ${{ github.ref_name }}
|
|
run: RELEASE_SHA="$(git rev-parse HEAD)" node scripts/ci/verify-release-gates.js
|
|
|
|
- name: Install dependencies
|
|
run: npm ci --ignore-scripts
|
|
|
|
- name: Run supply-chain IOC scan
|
|
run: npm run security:ioc-scan
|
|
|
|
- name: Verify OpenCode package payload
|
|
run: node tests/scripts/build-opencode.test.js
|
|
|
|
- name: Validate version tag
|
|
run: |
|
|
if ! [[ "${REF_NAME}" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]]; then
|
|
echo "Invalid version tag format. Expected vX.Y.Z or vX.Y.Z-prerelease"
|
|
exit 1
|
|
fi
|
|
|
|
env:
|
|
REF_NAME: ${{ github.ref_name }}
|
|
- name: Verify package version matches tag
|
|
env:
|
|
TAG_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
TAG_VERSION="${TAG_NAME#v}"
|
|
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
if [ "$TAG_VERSION" != "$PACKAGE_VERSION" ]; then
|
|
echo "::error::Tag version ($TAG_VERSION) does not match package.json version ($PACKAGE_VERSION)"
|
|
echo "Run: ./scripts/release.sh $TAG_VERSION"
|
|
exit 1
|
|
fi
|
|
|
|
- name: Verify VERSION matches tag
|
|
env:
|
|
TAG_NAME: ${{ github.ref_name }}
|
|
run: |
|
|
TAG_VERSION="${TAG_NAME#v}"
|
|
FILE_VERSION=$(tr -d '[:space:]' < VERSION)
|
|
if [ "$TAG_VERSION" != "$FILE_VERSION" ]; then
|
|
echo "::error::Tag version ($TAG_VERSION) does not match VERSION ($FILE_VERSION)"
|
|
exit 1
|
|
fi
|
|
|
|
# pi/core/package.json derives its version from VERSION, so a current
|
|
# profile also proves pi/core is in sync with the tag.
|
|
- name: Verify pi/core profile is current
|
|
run: node scripts/build-pi-core.js --check
|
|
|
|
- name: Verify release metadata stays in sync
|
|
run: node tests/plugin-manifest.test.js
|
|
|
|
- name: Check npm publish state
|
|
id: npm_publish_state
|
|
run: |
|
|
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
NPM_DIST_TAG=$(node -p "require('./package.json').version.includes('-') ? 'next' : 'latest'")
|
|
NPM_PUBLISH_TAG=$(node -p "require('./package.json').version.includes('-') ? 'next' : 'staged'")
|
|
set +e
|
|
NPM_LOOKUP=$(npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" version 2>&1)
|
|
NPM_STATUS=$?
|
|
set -e
|
|
if [ "$NPM_STATUS" -eq 0 ]; then
|
|
echo "already_published=true" >> "$GITHUB_OUTPUT"
|
|
elif printf '%s\n' "$NPM_LOOKUP" | grep -q 'E404'; then
|
|
echo "already_published=false" >> "$GITHUB_OUTPUT"
|
|
else
|
|
echo "::error::npm registry lookup failed; refusing to infer that the version is unpublished"
|
|
printf '%s\n' "$NPM_LOOKUP"
|
|
exit "$NPM_STATUS"
|
|
fi
|
|
echo "package_name=${PACKAGE_NAME}" >> "$GITHUB_OUTPUT"
|
|
echo "package_version=${PACKAGE_VERSION}" >> "$GITHUB_OUTPUT"
|
|
echo "dist_tag=${NPM_DIST_TAG}" >> "$GITHUB_OUTPUT"
|
|
echo "publish_tag=${NPM_PUBLISH_TAG}" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Use reviewed release notes
|
|
env:
|
|
RELEASE_TAG: ${{ github.ref_name }}
|
|
run: |
|
|
RELEASE_VERSION="${RELEASE_TAG#v}"
|
|
RELEASE_NOTES="docs/releases/${RELEASE_VERSION}/release-notes.md"
|
|
if [ ! -f "$RELEASE_NOTES" ]; then
|
|
echo "::error::Missing reviewed release notes for ${RELEASE_VERSION}: ${RELEASE_NOTES}"
|
|
exit 1
|
|
fi
|
|
cp "$RELEASE_NOTES" release_body.md
|
|
|
|
- name: Pack npm artifact
|
|
id: pack
|
|
run: |
|
|
npm pack --json > npm-pack.json
|
|
node -e "const crypto = require('crypto'); const fs = require('fs'); const data = JSON.parse(fs.readFileSync('npm-pack.json', 'utf8')); const entries = Array.isArray(data) ? data : [data]; const file = entries.find(entry => /^ecc-universal-[0-9A-Za-z.+-]+\.tgz$/.test(entry?.filename || ''))?.filename; if (!file) throw new Error('Unexpected packed filename'); const archives = fs.readdirSync('.').filter(name => name.endsWith('.tgz')); if (archives.length !== 1 || archives[0] !== file) throw new Error('Expected exactly one packed archive'); const digest = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); fs.appendFileSync(process.env.GITHUB_OUTPUT, 'package_file=' + file + '\npackage_sha256=' + digest + '\n')"
|
|
|
|
- name: Upload release artifacts
|
|
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
|
|
with:
|
|
name: ecc-release-artifacts
|
|
path: |
|
|
release_body.md
|
|
${{ steps.pack.outputs.package_file }}
|
|
tests/ci/packed-artifact-lifecycle.js
|
|
if-no-files-found: error
|
|
|
|
- name: Verify existing npm artifact matches candidate
|
|
if: steps.npm_publish_state.outputs.already_published == 'true'
|
|
env:
|
|
ECC_RELEASE_PACKAGE: ${{ steps.pack.outputs.package_file }}
|
|
run: |
|
|
PACKAGE_NAME=$(node -p "require('./package.json').name")
|
|
PACKAGE_VERSION=$(node -p "require('./package.json').version")
|
|
REGISTRY_INTEGRITY=$(npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" dist.integrity)
|
|
ECC_REGISTRY_INTEGRITY="$REGISTRY_INTEGRITY" node -e "const crypto = require('crypto'); const fs = require('fs'); const expected = process.env.ECC_REGISTRY_INTEGRITY; if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(expected || '')) throw new Error('Invalid registry integrity'); const actual = 'sha512-' + crypto.createHash('sha512').update(fs.readFileSync(process.env.ECC_RELEASE_PACKAGE)).digest('base64'); if (actual !== expected) throw new Error('Existing npm artifact does not match tested candidate')"
|
|
|
|
lifecycle:
|
|
name: Packed Lifecycle (${{ matrix.os }})
|
|
needs: verify
|
|
permissions:
|
|
contents: read
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
|
runs-on: ${{ matrix.os }}
|
|
|
|
steps:
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: '20.x'
|
|
|
|
- name: Download exact packed artifact
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: ecc-release-artifacts
|
|
path: release-artifacts
|
|
|
|
- name: Verify packed install lifecycle
|
|
env:
|
|
ECC_RELEASE_PACKAGE: release-artifacts/${{ needs.verify.outputs.package_file }}
|
|
ECC_RELEASE_SHA256: ${{ needs.verify.outputs.package_sha256 }}
|
|
run: node release-artifacts/tests/ci/packed-artifact-lifecycle.js
|
|
|
|
publish:
|
|
name: Publish Release
|
|
runs-on: ubuntu-latest
|
|
needs: [verify, lifecycle]
|
|
permissions:
|
|
contents: write
|
|
id-token: write
|
|
|
|
steps:
|
|
- name: Download release artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: ecc-release-artifacts
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
|
|
with:
|
|
node-version: '20.x'
|
|
registry-url: 'https://registry.npmjs.org'
|
|
|
|
- name: Verify artifact before publish
|
|
env:
|
|
ECC_RELEASE_PACKAGE: ${{ needs.verify.outputs.package_file }}
|
|
ECC_RELEASE_SHA256: ${{ needs.verify.outputs.package_sha256 }}
|
|
run: node -e "const crypto = require('crypto'); const fs = require('fs'); const file = process.env.ECC_RELEASE_PACKAGE; const expected = process.env.ECC_RELEASE_SHA256; if (!/^ecc-universal-[0-9A-Za-z.+-]+\.tgz$/.test(file || '')) throw new Error('Unexpected packed filename'); if (!/^[a-f0-9]{64}$/.test(expected || '')) throw new Error('Invalid packed SHA-256'); const archives = fs.readdirSync('.').filter(name => name.endsWith('.tgz')); if (archives.length !== 1 || archives[0] !== file) throw new Error('Expected exactly one downloaded archive'); const actual = crypto.createHash('sha256').update(fs.readFileSync(file)).digest('hex'); if (actual !== expected) throw new Error('Downloaded publish artifact SHA-256 mismatch')"
|
|
|
|
- name: Bind gate source to triggering commit
|
|
env:
|
|
EVENT_SHA: ${{ github.sha }}
|
|
VERIFIED_RELEASE_SHA: ${{ needs.verify.outputs.release_sha }}
|
|
run: node -e "const actual = process.env.EVENT_SHA; const expected = process.env.VERIFIED_RELEASE_SHA; if (typeof actual !== 'string' || actual.length !== 40 || !/^[a-f0-9]{40}$/.test(actual) || expected !== actual) throw new Error('Verified release differs from triggering commit')"
|
|
|
|
- name: Checkout verified gate source
|
|
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
|
|
with:
|
|
ref: ${{ github.sha }}
|
|
path: release-gate-source
|
|
persist-credentials: false
|
|
sparse-checkout: scripts/ci/verify-release-gates.js
|
|
sparse-checkout-cone-mode: false
|
|
|
|
# This read-only API check uses the existing publish job token. It is a
|
|
# snapshot; preventing subsequent tag movement requires protected tags.
|
|
- name: Recheck verified tag before publish
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
RELEASE_TAG: ${{ github.ref_name }}
|
|
RELEASE_SHA: ${{ needs.verify.outputs.release_sha }}
|
|
RELEASE_TAG_OBJECT_SHA: ${{ needs.verify.outputs.tag_object_sha }}
|
|
run: node release-gate-source/scripts/ci/verify-release-gates.js --tag-only
|
|
|
|
- name: Publish npm package
|
|
if: needs.verify.outputs.already_published != 'true'
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
ECC_RELEASE_PACKAGE: ${{ needs.verify.outputs.package_file }}
|
|
NPM_PUBLISH_TAG: ${{ needs.verify.outputs.publish_tag }}
|
|
run: npm publish "./${ECC_RELEASE_PACKAGE}" --access public --provenance --tag "${NPM_PUBLISH_TAG}"
|
|
|
|
- name: Verify published npm artifact
|
|
env:
|
|
ECC_RELEASE_PACKAGE: ${{ needs.verify.outputs.package_file }}
|
|
PACKAGE_NAME: ${{ needs.verify.outputs.package_name }}
|
|
PACKAGE_VERSION: ${{ needs.verify.outputs.package_version }}
|
|
run: |
|
|
REGISTRY_INTEGRITY=""
|
|
for ATTEMPT in $(seq 1 30); do
|
|
set +e
|
|
REGISTRY_INTEGRITY=$(npm view "${PACKAGE_NAME}@${PACKAGE_VERSION}" dist.integrity 2>&1)
|
|
NPM_STATUS=$?
|
|
set -e
|
|
if [ "$NPM_STATUS" -eq 0 ]; then
|
|
break
|
|
fi
|
|
if [ "$ATTEMPT" -eq 30 ]; then
|
|
echo "::error::Published npm artifact was not readable after five minutes"
|
|
printf '%s\n' "$REGISTRY_INTEGRITY"
|
|
exit "$NPM_STATUS"
|
|
fi
|
|
sleep 10
|
|
done
|
|
ECC_REGISTRY_INTEGRITY="$REGISTRY_INTEGRITY" node -e "const crypto = require('crypto'); const fs = require('fs'); const expected = process.env.ECC_REGISTRY_INTEGRITY; if (!/^sha512-[A-Za-z0-9+/]+={0,2}$/.test(expected || '')) throw new Error('Invalid published registry integrity'); const actual = 'sha512-' + crypto.createHash('sha512').update(fs.readFileSync(process.env.ECC_RELEASE_PACKAGE)).digest('base64'); if (actual !== expected) throw new Error('Published npm artifact does not match tested candidate')"
|
|
|
|
- name: Promote verified npm version
|
|
env:
|
|
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
PACKAGE_NAME: ${{ needs.verify.outputs.package_name }}
|
|
PACKAGE_VERSION: ${{ needs.verify.outputs.package_version }}
|
|
NPM_DIST_TAG: ${{ needs.verify.outputs.dist_tag }}
|
|
run: npm dist-tag add "${PACKAGE_NAME}@${PACKAGE_VERSION}" "${NPM_DIST_TAG}"
|
|
|
|
- name: Create GitHub Release
|
|
uses: softprops/action-gh-release@efb35369e0ad2afab669f228072c1b0d510eae64 # v3.0.3
|
|
with:
|
|
body_path: release_body.md
|
|
generate_release_notes: false
|
|
prerelease: ${{ contains(github.ref_name, '-') }}
|
|
make_latest: ${{ contains(github.ref_name, '-') && 'false' || 'true' }}
|