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
2 changes: 2 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ on:

jobs:
build:
# only run in forks — non-fork PRs get a build via preview-deployment.yml
if: github.event.pull_request.head.repo.full_name != github.repository
runs-on: ubuntu-latest
permissions:
contents: read
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/deploy-ic.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ jobs:
- run: npm run build

- name: Install icp-cli
run: npm i -g @icp-sdk/icp-cli@0.2.0 @icp-sdk/ic-wasm
run: npm i -g @icp-sdk/icp-cli@0.2.6 @icp-sdk/ic-wasm

- name: Import deploy identity
run: |
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/pr-cleanup.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: PR Cleanup
on:
pull_request:
types: [closed]

jobs:
release_preview_canister:
# do not run in forks
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
concurrency:
group: pr-${{ github.event.pull_request.number || github.event.number }}
cancel-in-progress: true

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1
- uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1
with:
python-version: "3.10"
- run: |
pip install icp-py-core "cbor2<6"
python3 .github/workflows/scripts/release-canister.py ${{ github.event.pull_request.number }}
env:
POOL_CONTROLLER_IDENTITY: ${{ secrets.POOL_CONTROLLER_IDENTITY }}
POOL_CANISTER_ID: ${{ secrets.POOL_CANISTER_ID }}

- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.delete(context, github, maybeComment.id);
}
95 changes: 95 additions & 0 deletions .github/workflows/preview-deployment.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
name: PR Preview Deployment
on:
pull_request:
types: [opened, synchronize, reopened]

jobs:
build_and_deploy:
# do not run in forks
if: github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
concurrency:
group: pr-${{ github.event.pull_request.number || github.event.number }}
cancel-in-progress: true

steps:
- uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1

- name: Initialize examples submodule
run: |
git config --global url."https://github.com/".insteadOf "git@github.com:"
git submodule update --init --depth 1 .sources/examples

- uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4.4.0
with:
node-version: 22
cache: npm

- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.update(context, github, maybeComment.id, `🤖 Your PR preview is being built...`);
} else {
await comments.create(context, github, `🤖 Your PR preview is being built...`);
}

- uses: actions/setup-python@7f4fc3e22c37d6ff65e88745f38bd3157c663f7c # v4.9.1
with:
python-version: "3.10"

- name: Install icp-cli
run: npm i -g @icp-sdk/icp-cli@0.2.6 @icp-sdk/ic-wasm

- run: npm ci

- name: Build & Deploy
run: |
# Setup identity
mkdir -p ~/.local/share/icp-cli/identity/keys
echo $POOL_CONTROLLER_IDENTITY | base64 -d > ~/.local/share/icp-cli/identity/keys/preview-deploy.pem
sed -i 's/\\r\\n/\r\n/g' ~/.local/share/icp-cli/identity/keys/preview-deploy.pem
icp identity import preview-deploy --from-pem ~/.local/share/icp-cli/identity/keys/preview-deploy.pem --storage plaintext
icp identity default preview-deploy

# Request preview canister from the pool
pip install icp-py-core "cbor2<6"
canister_id=$(python3 .github/workflows/scripts/request-canister.py ${{ github.event.pull_request.number }})

# Override canister ID mapping for ic environment
echo "{\"frontend\":\"$canister_id\"}" > .icp/data/mappings/ic.ids.json

echo "PREVIEW_CANISTER_ID=$canister_id" >> $GITHUB_ENV

# Deploy (icp.yaml recipe handles the build)
icp deploy frontend -e ic --mode reinstall

env:
POOL_CONTROLLER_IDENTITY: ${{ secrets.POOL_CONTROLLER_IDENTITY }}
POOL_CANISTER_ID: ${{ secrets.POOL_CANISTER_ID }}

- name: Report build error
uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
if: ${{ failure() }}
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.update(context, github, maybeComment.id, `🤖 Preview build failed.`);
} else {
await comments.create(context, github, `🤖 Preview build failed.`);
}

- uses: actions/github-script@d7906e4ad0b1822421a7e6a35d5ca353c962f410 # v6.4.1
with:
script: |
const comments = require('./.github/workflows/scripts/comments.cjs');
const maybeComment = await comments.get(context, github);
if (maybeComment) {
await comments.update(context, github, maybeComment.id, `🤖 Here's your preview: https://${process.env.PREVIEW_CANISTER_ID}.icp0.io`);
} else {
await comments.create(context, github, `🤖 Here's your preview: https://${process.env.PREVIEW_CANISTER_ID}.icp0.io`);
}
39 changes: 39 additions & 0 deletions .github/workflows/scripts/comments.cjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
const MARKER = '<!-- pr-preview -->';

exports.get = async function (context, github) {
const comments = await github.rest.issues.listComments({
issue_number: context.issue.number,
repo: context.repo.repo,
owner: context.repo.owner,
});

return comments.data.find(
Comment thread
marc0olo marked this conversation as resolved.
(c) => c.user.login === 'github-actions[bot]' && c.user.type === 'Bot' && c.body.includes(MARKER)
);
};

exports.create = function (context, github, body) {
return github.rest.issues.createComment({
issue_number: context.issue.number,
owner: context.repo.owner,
repo: context.repo.repo,
body: `${MARKER}\n${body}`,
});
};

exports.update = function (context, github, id, body) {
return github.rest.issues.updateComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: id,
body: `${MARKER}\n${body}`,
});
};

exports.delete = function (context, github, id) {
return github.rest.issues.deleteComment({
owner: context.repo.owner,
repo: context.repo.repo,
comment_id: id,
});
};
30 changes: 30 additions & 0 deletions .github/workflows/scripts/pool.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
from icp_core import Agent, Client, Identity, encode, Types
import os
import sys
import base64


#
# Interact with preview canister pool: https://github.com/dfinity/preview-canister-pool
#

private_key = base64.b64decode(os.environ["POOL_CONTROLLER_IDENTITY"]).decode("utf-8")
pool_id = os.environ["POOL_CANISTER_ID"]

identity = Identity.from_pem(private_key)
client = Client()
agent = Agent(identity, client)

def release_canister():
res = agent.update_raw(
pool_id, "release_canister", encode([{'type': Types.Text, 'value': sys.argv[1]}]),
verify_certificate=False)
return res


def request_canister():
res = agent.update_raw(
pool_id, "request_canister", encode([{'type': Types.Text, 'value': sys.argv[1]}]),
return_type=Types.Principal,
verify_certificate=False)
return res
22 changes: 22 additions & 0 deletions .github/workflows/scripts/release-canister.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
import os
import sys
import traceback

if len(sys.argv) != 2:
print("Usage: python3 release-canister.py <ref>")
exit(1)

for v in ["POOL_CONTROLLER_IDENTITY","POOL_CANISTER_ID"]:
if not v in os.environ:
print(f"release-canister.py: {v} env variable missing")
exit(1)


from pool import release_canister

try:
release_canister()
except Exception as e:
print(f"release-canister.py: failed to release canister: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
exit(1)
25 changes: 25 additions & 0 deletions .github/workflows/scripts/request-canister.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import os
import sys
import traceback

if len(sys.argv) != 2:
print("Usage: python3 request_canister.py <ref>")
exit(1)

for v in ["POOL_CONTROLLER_IDENTITY","POOL_CANISTER_ID"]:
if not v in os.environ:
print(f"request-canister.py: {v} env variable missing")
exit(1)

from pool import request_canister

try:
result = request_canister()
canister_id = result[0]['value'].to_str()
print(canister_id)
except Exception as e:
print(f"request-canister.py: failed to request canister: {e}", file=sys.stderr)
traceback.print_exc(file=sys.stderr)
if 'result' in dir():
print(f"request-canister.py: raw result: {result}", file=sys.stderr)
exit(1)
Loading