diff --git a/README.md b/README.md
new file mode 100644
index 0000000..f72c539
--- /dev/null
+++ b/README.md
@@ -0,0 +1,25 @@
+# Snapcrafters CI
+
+This repository contains common actions and tools used throughout the Snapcrafters
+[organisation](https://github.com/snapcrafters) for the testing and delivery of our snaps.
+
+## Snapcrafters Actions
+
+The actions in this repo are all used during the build, test and release of our snaps. Each of them
+listed below has it's own README
+
+- [snapcrafters/ci/call-for-testing](call-for-testing/README.md)
+- [snapcrafters/ci/get-architectures](get-architectures/README.md)
+- [snapcrafters/ci/get-screenshots](get-screenshots/README.md)
+- [snapcrafters/ci/promote-to-stable](promote-to-stable/README.md)
+- [snapcrafters/ci/release-to-candidate](release-to-candidate/README.md)
+- [snapcrafters/ci/sync-version](sync-version/README.md)
+- [snapcrafters/ci/test-snap-build](test-snap-build/README.md)
+
+### Usage
+
+You can see examples of these actions in use in the following repos:
+
+- [signal-desktop](https://github.com/snapcrafters/signal-desktop/main/.github/workflows)
+- [mattermost-desktop](https://github.com/snapcrafters/mattermost-desktop/main/.github/workflows)
+- [discord](https://github.com/snapcrafters/discord/main/.github/workflows)
diff --git a/call-for-testing/README.md b/call-for-testing/README.md
new file mode 100644
index 0000000..e82ec61
--- /dev/null
+++ b/call-for-testing/README.md
@@ -0,0 +1,75 @@
+# snapcrafters/ci/call-for-testing
+
+Automatically creates a templated call for testing as a Github issue, containing the details of
+newly released revisions and instructions on how to test and promote them.
+
+## Usage
+
+### Use in combination with `snapcrafters/ci/release-to-candidate`
+
+In this mode, the action will look for an artifact uploaded by the
+`snapcrafters/ci/release-to-candidate` action that contains a manifest detailing the exact
+revisions that were uploaded, and use those to populate the call for testing template.
+
+```yaml
+jobs:
+ release:
+ name: ๐ข Release to latest/candidate
+ runs-on: ubuntu-latest
+ steps:
+ - name: ๐ข Release to latest/candidate
+ uses: snapcrafters/ci/release-to-candidate@main
+ with:
+ architecture: arm64
+ launchpad-token: ${{ secrets.LAUNCHPAD_TOKEN }}
+ store-token: ${{ secrets.STORE_TOKEN }}
+
+ call-for-testing:
+ name: ๐ฃ Create call for testing
+ needs: release
+ runs-on: ubuntu-latest
+ steps:
+ - name: ๐ฃ Create call for testing
+ uses: snapcrafters/ci/call-for-testing@main
+ with:
+ architectures: "amd64 arm64"
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+```
+
+### Use standalone - `store-token` required
+
+In this mode, the action will use the store token to fetch the latest revision on the specified
+channel (`candidate` by default) for each architecture and populate the call for testing with those
+revisions.
+
+```yaml
+jobs:
+ call-for-testing:
+ name: ๐ฃ Create call for testing
+ runs-on: ubuntu-latest
+ steps:
+ - name: ๐ฃ Create call for testing
+ uses: snapcrafters/ci/call-for-testing@main
+ with:
+ architectures: "amd64 arm64"
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ store-token: ${{ secrets.STORE_TOKEN }}
+```
+
+## API
+
+### Inputs
+
+| Key | Description | Required | Default |
+| --------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------: | :---------------- |
+| `architectures` | The architectures that the snap supports. | Y | |
+| `ci-repo` | The repo to fetch tools/templates from. Only for debugging. | N | `snapcrafters/ci` |
+| `channel` | The channel to create the call for testing for. | N | `candidate` |
+| `github-token` | A token with permissions to create issues on the repository. | Y | |
+| `store-token` | A token with permissions to query the specified channel in the Snap Store. Only required if the revisions to test are not passed to the workflow by the `release-to-candidate` workflow | N | |
+
+### Outputs
+
+| Key | Description | Example |
+| -------------- | ------------------------------------------------- | ------- |
+| `issue-number` | The issue number containing the call for testing. | `12` |
diff --git a/call-for-testing/action.yaml b/call-for-testing/action.yaml
new file mode 100644
index 0000000..3b0cc40
--- /dev/null
+++ b/call-for-testing/action.yaml
@@ -0,0 +1,113 @@
+name: Create call for testing
+description: Create a call for testing for a given snap repository
+author: Snapcrafters
+branding:
+ icon: message-circle
+ color: orange
+
+inputs:
+ architectures:
+ description: "The architectures to build the snap for"
+ required: true
+ ci-repo:
+ description: "The repo to fetch tools/templates from. Only for debugging"
+ default: "snapcrafters/ci"
+ required: false
+ channel:
+ description: "The channel to publish the snap to"
+ default: "candidate"
+ required: false
+ github-token:
+ description: "A token with permissions to create issues on the repository"
+ required: true
+ store-token:
+ description: "A token with permissions to upload to the specified channel"
+ required: false
+
+outputs:
+ issue-number:
+ description: "The issue number containing the call for testing"
+ value: ${{ steps.issue.outputs.number }}
+
+runs:
+ using: composite
+ steps:
+ - name: Checkout the source
+ uses: actions/checkout@v4
+
+ - name: Download manifest files
+ continue-on-error: true
+ uses: actions/download-artifact@v3
+ with:
+ name: "manifests"
+
+ - name: Setup snapcraft
+ shell: bash
+ run: |
+ sudo snap install snapcraft --classic
+
+ - name: Write the arch/rev table
+ shell: bash
+ id: build
+ env:
+ SNAPCRAFT_STORE_CREDENTIALS: ${{ inputs.store-token }}
+ SNAP_NAME: ${{ github.event.repository.name }}
+ run: |
+ revisions=()
+
+ # Build the initial structure for the HTML table including the header row.
+ table="
CPU Architecture | Revision |
"
+
+ # If we were able to fetch build manifests from previous step, use those
+ if ls -l manifest-*.yaml &>/dev/null; then
+ echo "Found build manifests - populating template with revisions from the manifests"
+
+ # Iterate over the manifest files and write the table rows for each architecture
+ for file in $(ls manifest-*.yaml); do
+ # Parse the arch and the revision
+ arch="$(cat "${file}" | yq -r '.architecture')"
+ rev="$(cat "${file}" | yq -r '.revision')"
+ # Write the table row and add the revision to the list we're tracking
+ table="${table}${arch} | ${rev} |
"
+ revisions+=("$rev")
+ done
+ else
+ echo "No build manifests found - populating template with information from the store"
+
+ # Otherwise, get the latest revision for each architecture in the release channel
+ for arch in ${{ inputs.architectures }}; do
+ rev="$(snapcraft list-revisions "${SNAP_NAME}" --arch "$arch" | grep "latest/${{ inputs.channel }}*" | head -n1 | cut -d' ' -f1)"
+ revisions+=("$rev")
+ # Add a row to the HTML table
+ table="${table}${arch} | ${rev} |
"
+ done
+ fi
+
+ # Add the closing tags for the table
+ table="${table}
"
+
+ # Get a comma separated list of revisions
+ printf -v joined '%s,' "${revisions[@]}"
+
+ version="$(cat snap/snapcraft.yaml | yq -r '.version')"
+ echo "version=${version}" >> "$GITHUB_OUTPUT"
+ echo "revisions=${joined%,}" >> "$GITHUB_OUTPUT"
+ echo "table=${table}" >> "$GITHUB_OUTPUT"
+
+ - name: Fetch the call for testing template
+ shell: bash
+ run: |
+ wget -qO template.md "https://raw.githubusercontent.com/${{ inputs.ci-repo }}/main/call-for-testing/template.md"
+
+ - name: Create call for testing issue
+ uses: JasonEtco/create-an-issue@v2
+ id: issue
+ env:
+ GITHUB_TOKEN: ${{ inputs.github-token }}
+ snap_name: ${{ github.event.repository.name }}
+ channel: ${{ inputs.channel }}
+ revisions: ${{ steps.build.outputs.revisions }}
+ table: ${{ steps.build.outputs.table }}
+ version: ${{ steps.build.outputs.version }}
+ with:
+ filename: ./template.md
diff --git a/call-for-testing/template.md b/call-for-testing/template.md
new file mode 100644
index 0000000..2500af7
--- /dev/null
+++ b/call-for-testing/template.md
@@ -0,0 +1,44 @@
+---
+title: Call for testing `{{ env.snap_name }}`
+labels: testing
+---
+
+A new version ({{ env.version }}) of `{{ env.snap_name }}` was just pushed to the `{{ env.channel }}` channel [in the snap store](https://snapcraft.io/{{ env.snap_name }}). The following revisions are available.
+
+{{ env.table }}
+
+## Automated screenshots
+
+The snap will be installed in a VM automatically; screenshots will be posted as a comment on this issue shortly.
+
+## How to test it manually
+
+1. Stop the application if it was already running
+1. Upgrade to this version by running
+
+ ```shell
+ snap refresh {{ env.snap_name }} --{{ env.channel }}
+ ```
+
+1. Start the app and test it out.
+1. Finally, add a comment below explaining whether this app is working, and **include the output of the following command**.
+
+ ```shell
+ snap version; lscpu | grep Architecture; snap info {{ env.snap_name }} | grep installed
+ ```
+
+## How to release it
+
+Maintainers can promote this to stable by commenting `/promote [,] stable [done]`.
+
+> For example
+>
+> - To promote a single revision, run `/promote stable`
+> - To promote multiple revisions, run `/promote , stable`
+> - To promote a revision and close the issue, run `/promote , stable done`
+
+You can promote all revisions that were just built with:
+
+```
+/promote {{ env.revisions }} stable done
+```
diff --git a/get-architectures/README.md b/get-architectures/README.md
new file mode 100644
index 0000000..f38f14c
--- /dev/null
+++ b/get-architectures/README.md
@@ -0,0 +1,34 @@
+# snapcrafters/ci/get-architectures
+
+Parses a `snapcraft.yaml` and returns the list of architectures supported both as a JSON array, and
+a space-separated string.
+
+## Usage
+
+```yaml
+# ...
+jobs:
+ get-architectures:
+ name: ๐ฅ Get snap architectures
+ runs-on: ubuntu-latest
+ outputs:
+ architectures: ${{ steps.get-architectures.outputs.architectures }}
+ architectures-list: ${{ steps.get-architectures.outputs.architectures-list }}
+ steps:
+ - name: ๐ฅ Get snap architectures
+ id: get-architectures
+ uses: snapcrafters/ci/get-architectures@main
+```
+
+## API
+
+### Inputs
+
+None
+
+### Outputs
+
+| Key | Description | Example |
+| -------------------- | -------------------------------------------------------------- | -------------------- |
+| `architectures` | A space-separated list of architectures supported by the snap. | `amd64 arm64` |
+| `architectures-list` | A JSON list of architectures supported by the snap | `["amd64", "arm64"]` |
diff --git a/get-architectures/action.yaml b/get-architectures/action.yaml
new file mode 100644
index 0000000..7a121e4
--- /dev/null
+++ b/get-architectures/action.yaml
@@ -0,0 +1,39 @@
+name: Get Architectures
+description: Get the architectures supported by a given snap
+author: Snapcrafters
+branding:
+ icon: code
+ color: orange
+
+outputs:
+ architectures:
+ description: "A space-separated list of architectures supported by the snap"
+ value: ${{ steps.architectures.outputs.architectures }}
+ architectures-list:
+ description: "A JSON list of architectures supported by the snap"
+ value: ${{ steps.architectures.outputs.architectures_list }}
+
+runs:
+ using: composite
+ steps:
+ - name: Checkout the source
+ uses: actions/checkout@v4
+
+ - name: Compute architectures
+ id: architectures
+ shell: bash
+ run: |
+ # Get the list as a json array. E.g. ["amd64", "arm64"]
+ architectures_list="$(cat snap/snapcraft.yaml | yq -r -I=0 -o=json '[.architectures[]]')"
+
+ # Get the list as a space-separated string. E.g. "amd64" "arm64"
+ architectures="$(cat snap/snapcraft.yaml | yq -r -I=0 -o=csv '[.architectures[]]' | tr ',' ' ')"
+
+ # Handle the case where architectures is a list of objects
+ if echo "$architectures" | grep -q "build-on"; then
+ architectures_list="$(cat snap/snapcraft.yaml | yq -r -I=0 -o=json '[.architectures[]."build-on"]')"
+ architectures="$(cat snap/snapcraft.yaml | yq -r -I=0 -o=csv '[.architectures[]."build-on"]' | tr ',' ' ')"
+ fi
+
+ echo "architectures_list=$architectures_list" >> "$GITHUB_OUTPUT"
+ echo "architectures=$architectures" >> "$GITHUB_OUTPUT"
diff --git a/get-screenshots/README.md b/get-screenshots/README.md
new file mode 100644
index 0000000..edc91da
--- /dev/null
+++ b/get-screenshots/README.md
@@ -0,0 +1,44 @@
+# snapcrafters/ci/get-screenshots
+
+Deploys the snap from `latest/candidate` in a LXD desktop VM, then takes screenshots of the whole
+desktop, and the most recent active window after the snap was launched. Screenshots are then
+committed to [ci-screenshots](https://github.com/snapcrafters/ci-screenshots), and added to a comment on
+the original call for testing issue.
+
+## Usage
+
+```yaml
+# ...
+jobs:
+ screenshots:
+ name: ๐ธ Gather screenshots
+ needs: call-for-testing
+ runs-on: ubuntu-latest
+ steps:
+ - name: ๐ธ Gather screenshots
+ uses: snapcrafters/ci/get-screenshots@main
+ with:
+ issue-number: ${{ needs.call-for-testing.outputs.issue-number }}
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ screenshots-token: ${{ secrets.SCREENSHOT_COMMIT_TOKEN }}
+```
+
+## API
+
+### Inputs
+
+| Key | Description | Required | Default |
+| ------------------- | ------------------------------------------------------------------------------------------------------------------ | :------: | :---------------------------- |
+| `issue-number` | The issue number to post the screenshots to. | Y | |
+| `ci-repo` | The repo to fetch tools/templates from. Only for debugging. | N | `snapcrafters/ci` |
+| `channel` | The channel to create the call for testing for. | N | `candidate` |
+| `github-token` | A token with permissions to common on issues in the repository. | Y | |
+| `screenshots-repo` | The repository where screenshots should be uploaded. | N | `snapcrafters/ci-screenshots` |
+| `screenshots-token` | A token with permissions to commit screenshots to [ci-screenshots](https://github.com/snapcrafters/ci-screenshots) | Y | |
+
+### Outputs
+
+| Key | Description |
+| -------- | ------------------------------------------------------------- |
+| `screen` | A URL pointing to a screenshot of the whole screen in the VM |
+| `window` | A URL pointing to a screenshot of the active window in the VM |
diff --git a/get-screenshots/action.yaml b/get-screenshots/action.yaml
new file mode 100644
index 0000000..cb7add2
--- /dev/null
+++ b/get-screenshots/action.yaml
@@ -0,0 +1,130 @@
+name: Get screenshots
+description: Install a snap in a VM and get screenshots of it running
+author: Snapcrafters
+branding:
+ icon: camera
+ color: orange
+
+inputs:
+ issue-number:
+ description: "The issue number to post the screenshots into"
+ required: true
+ ci-repo:
+ description: "The repo to fetch tools/templates from. Only for debugging."
+ default: "snapcrafters/ci"
+ required: false
+ channel:
+ description: "The channel to publish the snap to"
+ default: "candidate"
+ required: false
+ github-token:
+ description: "A token with permissions to comment on issues"
+ required: true
+ screenshots-repo:
+ description: "The repository where screenshots should be uploaded."
+ default: "snapcrafters/ci-screenshots"
+ required: false
+ screenshots-token:
+ description: "A token with permissions to commit files to the screenshots repo"
+ required: true
+
+outputs:
+ screen:
+ description: "URL to a screenshot of the full screen of the VM"
+ value: ${{ steps.screenshots.outputs.screen }}
+ window:
+ description: "URL to a screenshot of the full screen of the VM"
+ value: ${{ steps.screenshots.outputs.window }}
+
+runs:
+ using: composite
+ steps:
+ - name: Checkout the code
+ uses: actions/checkout@v4
+
+ - name: Setup ghvmctl
+ uses: snapcrafters/ci/setup-ghvmctl@main
+
+ - name: Download manifest files (if available)
+ continue-on-error: true
+ uses: actions/download-artifact@v3
+ with:
+ name: manifests
+
+ - name: Prepare VM
+ shell: bash
+ env:
+ SNAP_NAME: ${{ github.event.repository.name }}
+ run: |
+ ghvmctl prepare
+
+ # If we got a manifest file then parse the revision from it
+ if ls manifest-amd64.yaml &>/dev/null; then
+ rev="$(cat manifest-amd64.yaml | yq -r '.revision')"
+ echo "Installing snap revision '${rev}' from build manifest"
+ ghvmctl install-snap "${SNAP_NAME}" --revision "${rev}"
+ else
+ echo "Installing snap from 'latest/${{ inputs.channel}}'"
+ ghvmctl install-snap "${SNAP_NAME}" --channel "latest/${{ inputs.channel }}"
+ fi
+
+ ghvmctl run-snap "${SNAP_NAME}"
+ sleep 60
+
+ - name: Gather screenshots
+ shell: bash
+ run: |
+ ghvmctl screenshot-full
+ ghvmctl screenshot-window
+
+ - name: Output application logs
+ shell: bash
+ env:
+ SNAP_NAME: ${{ github.event.repository.name }}
+ run: |
+ ghvmctl exec "cat /home/ubuntu/${SNAP_NAME}.log"
+
+ - name: Checkout screenshots repo
+ uses: actions/checkout@v4
+ with:
+ repository: ${{ inputs.screenshots-repo }}
+ path: ci-screenshots
+ token: ${{ inputs.screenshots-token }}
+
+ - name: Upload screenshots to screenshots repo
+ shell: bash
+ id: screenshots
+ env:
+ SNAP_NAME: ${{ github.event.repository.name }}
+ run: |
+ file_prefix="$(date +%Y%m%d)-${SNAP_NAME}-${{ inputs.issue-number }}"
+
+ pushd ci-screenshots
+ mv "$HOME/ghvmctl-screenshots/screenshot-screen.png" "${file_prefix}-screen.png"
+ mv "$HOME/ghvmctl-screenshots/screenshot-window.png" "${file_prefix}-window.png"
+
+ git config --global user.email "merlijn.sebrechts+snapcrafters-bot@gmail.com"
+ git config --global user.name "Snapcrafters Bot"
+
+ git add -A .
+ git commit -m "data: screenshots for snapcrafters/${SNAP_NAME}#${{ inputs.issue-number }}"
+ git push origin main
+
+ echo "screen=https://raw.githubusercontent.com/${{ inputs.screenshots-repo }}/main/${file_prefix}-screen.png" >> "$GITHUB_OUTPUT"
+ echo "window=https://raw.githubusercontent.com/${{ inputs.screenshots-repo }}/main/${file_prefix}-window.png" >> "$GITHUB_OUTPUT"
+
+ - name: Comment on call for testing issue with screenshots
+ uses: actions/github-script@v7
+ with:
+ script: |
+ github.rest.issues.createComment({
+ issue_number: ${{ inputs.issue-number }},
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: `The following screenshots were taken during automated testing:
+
+ ![window](${{ steps.screenshots.outputs.window }})
+
+ ![screen](${{ steps.screenshots.outputs.screen }})
+ `
+ })
diff --git a/promote-to-stable/README.md b/promote-to-stable/README.md
new file mode 100644
index 0000000..6ed3446
--- /dev/null
+++ b/promote-to-stable/README.md
@@ -0,0 +1,37 @@
+# snapcrafters/ci/promote-to-stable
+
+Promote to stable is generally triggered in response to a Snapcrafters reviewer posting a comment
+containing a `/promote` command. Once the arguments are successfully parsed, the specified
+revisions are promoted to `latest/stable`
+
+## Usage
+
+```yaml
+# ...
+jobs:
+ promote:
+ name: โฌ๏ธ Promote to stable
+ runs-on: ubuntu-latest
+ if: |
+ ( !github.event.issue.pull_request )
+ && contains(github.event.comment.body, '/promote ')
+ && contains(github.event.*.labels.*.name, 'testing')
+ steps:
+ - name: โฌ๏ธ Promote to stable
+ uses: snapcrafters/ci/promote-to-stable@main
+ with:
+ store-token: ${{ secrets.SNAP_STORE_STABLE }}
+```
+
+## API
+
+### Inputs
+
+| Key | Description | Required | Default |
+| -------------- | ---------------------------------------------------------------------------------------- | :------: | :------ |
+| `github-token` | A token with permissions to write issues on the repository | Y | |
+| `store-token` | A token with permissions to upload and release to the `stable` channel in the Snap Store | Y | |
+
+### Outputs
+
+None
diff --git a/promote-to-stable/action.yaml b/promote-to-stable/action.yaml
new file mode 100644
index 0000000..acd5491
--- /dev/null
+++ b/promote-to-stable/action.yaml
@@ -0,0 +1,104 @@
+name: Promote to latest/stable
+description: Promotes a given set of revisions from candidate -> stable
+author: Snapcrafters
+branding:
+ icon: trending-up
+ color: orange
+
+inputs:
+ github-token:
+ description: "A token with permissions to write issues on the repository"
+ required: true
+ store-token:
+ description: "A token with permissions to upload to the specified channel"
+ required: true
+
+runs:
+ using: composite
+ steps:
+ - name: Parse slash command
+ id: command
+ uses: xt0rted/slash-command-action@v2
+ with:
+ repo-token: ${{ inputs.github-token }}
+ command: promote
+ reaction: "true"
+ reaction-type: "eyes"
+ allow-edits: "false"
+ permission-level: write
+
+ - name: Install snapcraft
+ shell: bash
+ run: |
+ sudo snap install --classic snapcraft
+
+ - name: Promote snap to latest/stable
+ id: promote
+ env:
+ SNAPCRAFT_STORE_CREDENTIALS: ${{ inputs.store-token }}
+ SNAP_NAME: ${{ github.event.repository.name }}
+ shell: bash
+ run: |
+ echo "The command was '${{ steps.command.outputs.command-name }}' with arguments '${{ steps.command.outputs.command-arguments }}'"
+
+ arguments=(${{ steps.command.outputs.command-arguments }})
+ revision=${arguments[0]}
+ channel=${arguments[1]}
+ done=${arguments[2]}
+
+ re='^[0-9]+([,][0-9]+)*$'
+ if [[ ! "$revision" =~ $re ]]; then
+ echo "revision must be a number or a comma separated list of numbers, not '$revision'!"
+ exit 1
+ fi
+
+ if [[ "$channel" != "stable" ]]; then
+ echo "I can only promote to stable, not '$channel'!"
+ exit 1
+ fi
+
+ if [[ -n "$done" && "$done" != "done" ]]; then
+ echo "The third argument should be 'done' or empty"
+ exit 1
+ fi
+
+ # Iterate over each specified revision and release
+ revs=$(echo $revision | tr "," "\n")
+ released_revs=()
+
+ for r in $revs; do
+ snapcraft release $SNAP_NAME "$r" "$channel"
+ released_revs+=("$r")
+ done
+
+ # Get a comma separated list of released revisions
+ printf -v joined '%s,' "${released_revs[@]}"
+
+ echo "revisions=${joined%,}" >> $GITHUB_OUTPUT
+ echo "channel=$channel" >> $GITHUB_OUTPUT
+ echo "done=$done" >> $GITHUB_OUTPUT
+
+ - name: Comment on call for testing issue
+ uses: actions/github-script@v7
+ with:
+ script: |
+ github.rest.issues.createComment({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ body: 'The following revisions were released to the `${{ steps.promote.outputs.channel }}` channel: `${{ steps.promote.outputs.revisions }}`'
+ })
+
+ - name: Close call for testing issue
+ if: ${{ steps.promote.outputs.done }} == "done"
+ uses: actions/github-script@v7
+ with:
+ script: |
+ if ("${{ steps.promote.outputs.done }}" === "done") {
+ github.rest.issues.update({
+ issue_number: context.issue.number,
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ state: 'closed'
+ })
+ }
diff --git a/release-to-candidate/README.md b/release-to-candidate/README.md
new file mode 100644
index 0000000..6bf00fb
--- /dev/null
+++ b/release-to-candidate/README.md
@@ -0,0 +1,38 @@
+# snapcrafters/ci/release-to-candidate
+
+This action is used to run `snapcraft remote-build` for a given Snap, and a given architecture.
+Following that, the snap is released to the `latest/candidate` automatically.
+
+## Usage
+
+```yaml
+# ...
+jobs:
+ release:
+ name: ๐ข Release to latest/candidate
+ runs-on: ubuntu-latest
+ steps:
+ - name: ๐ข Release to latest/candidate
+ uses: snapcrafters/ci/release-to-candidate@main
+ with:
+ architecture: arm64
+ launchpad-token: ${{ secrets.LAUNCHPAD_TOKEN }}
+ store-token: ${{ secrets.STORE_TOKEN }}
+```
+
+## API
+
+### Inputs
+
+| Key | Description | Required | Default |
+| ----------------- | ----------------------------------------------------------------------------------------- | :------: | :---------- |
+| `architecture` | The architecture for which to build the snap. | N | `amd64` |
+| `channel` | The channel to release the snap to. | N | `candidate` |
+| `launchpad-token` | A token with permissions to create Launchpad remote builds. | Y | |
+| `store-token` | A token with permissions to upload and release to the specified channel in the Snap Store | Y | |
+
+### Outputs
+
+| Key | Description | Example |
+| ---------- | ----------------------------------------- | ------- |
+| `revision` | The Snap Store revision that was created. | `15` |
diff --git a/release-to-candidate/action.yaml b/release-to-candidate/action.yaml
new file mode 100644
index 0000000..cd24d62
--- /dev/null
+++ b/release-to-candidate/action.yaml
@@ -0,0 +1,122 @@
+name: Release to Candidate
+description: Builds a snap using `snapcraft remote-build` and releases it to candidate
+author: Snapcrafters
+branding:
+ icon: tag
+ color: orange
+
+inputs:
+ architecture:
+ description: "The architecture to build the snap for"
+ default: amd64
+ required: false
+ channel:
+ description: "The channel to publish the snap to"
+ default: "candidate"
+ required: false
+ launchpad-token:
+ description: "A token with permissions to create remote builds on Launchpad"
+ required: true
+ store-token:
+ description: "A token with permissions to upload to the specified channel"
+ required: true
+
+outputs:
+ revision:
+ description: "The revision of the uploaded snap"
+ value: ${{ steps.publish.outputs.revision }}
+
+runs:
+ using: composite
+ steps:
+ - name: Checkout the source
+ uses: actions/checkout@v4
+
+ - name: Setup snapcraft
+ shell: bash
+ run: |
+ sudo snap install snapcraft --classic
+
+ # Setup Launchpad credentials
+ mkdir -p ~/.local/share/snapcraft/provider/launchpad
+ echo "${{ inputs.launchpad-token }}" > ~/.local/share/snapcraft/provider/launchpad/credentials
+ git config --global user.email "github-actions@github.com"
+ git config --global user.name "Github Actions"
+
+ # Install moreutils so we have access to sponge
+ sudo apt-get update; sudo apt-get install -y moreutils
+
+ - name: Build the snap (${{ inputs.architecture }})
+ id: build
+ shell: bash
+ env:
+ name: ${{ github.event.repository.name }}
+ arch: ${{ inputs.architecture }}
+ run : |
+ # Remove the architecture definition from the snapcraft.yaml due to:
+ # https://bugs.launchpad.net/snapcraft/+bug/1885150
+ cat snap/snapcraft.yaml | yq 'del(.architectures)' | sponge snap/snapcraft.yaml
+
+ snapcraft remote-build --launchpad-accept-public-upload --build-for="${arch}"
+
+ version="$(cat snap/snapcraft.yaml | yq -r '.version')"
+ echo "snap=${name}_${version}_${arch}.snap" >> "$GITHUB_OUTPUT"
+
+ # Write the manifest file which is used by later steps
+ echo "name: ${name}" >> "manifest-${arch}.yaml"
+ echo "architecture: ${arch}" >> "manifest-${arch}.yaml"
+
+ - name: Parse snap review information
+ id: parse
+ shell: bash
+ run : |
+ # Populate defaults
+ echo "classic=false" >> "$GITHUB_OUTPUT"
+ echo "slots=''" >> "$GITHUB_OUTPUT"
+ echo "plugs=''" >> "$GITHUB_OUTPUT"
+
+ # Check for classic confinement and update the output if the snap is classic
+ if [[ "$(cat snap/snapcraft.yaml | yq -r '.confinement')" == "classic" ]]; then
+ echo "classic=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ # Declare the common locations for plugs/slots declarations
+ plugs_files=("plug-declaration.json" ".github/plug-declaration.json")
+ slots_files=("slot-declaration.json" ".github/slot-declaration.json")
+
+ for file in "${plugs_files[@]}"; do
+ if [[ -f "$file" ]]; then
+ echo "plugs=$file" >> "$GITHUB_OUTPUT"
+ fi
+ done
+
+ for file in "${slots_files[@]}"; do
+ if [[ -f "$file" ]]; then
+ echo "slots=$file" >> "$GITHUB_OUTPUT"
+ fi
+ done
+
+ - name: Review the built snap
+ uses: diddlesnaps/snapcraft-review-action@v1
+ with:
+ snap: ${{ steps.build.outputs.snap }}
+ isClassic: ${{ steps.build.outputs.classic }}
+
+ - name: Release the built snap to latest/${{ inputs.channel }}
+ id: publish
+ shell: bash
+ env:
+ SNAPCRAFT_STORE_CREDENTIALS: ${{ inputs.store-token }}
+ SNAP_FILE: ${{ steps.build.outputs.snap }}
+ run: |
+ snapcraft_out="$(snapcraft push "$SNAP_FILE" --release="${{ inputs.channel }}")"
+ revision="$(echo "$snapcraft_out" | grep -Po "Revision \K[^ ]+")"
+ echo "revision=${revision}" >> "$GITHUB_OUTPUT"
+ echo "revision: ${revision}" >> "manifest-${{ inputs.architecture }}.yaml"
+
+ # Upload the manifest file as an artifact for retrieval during future actions
+ - name: Upload revision manifest
+ uses: actions/upload-artifact@v3.1.3
+ with:
+ name: "manifests"
+ path: "manifest-${{ inputs.architecture }}.yaml"
diff --git a/setup-ghvmctl/README.md b/setup-ghvmctl/README.md
new file mode 100644
index 0000000..c9980e2
--- /dev/null
+++ b/setup-ghvmctl/README.md
@@ -0,0 +1,48 @@
+# snapcrafters/ci/setup-ghvmctl
+
+A simple Github Action for configuring [ghvmctl](https://github.com/snapcrafters/ghvmctl) for use
+on Github Actions runners is also included in this repository. It has three major functions:
+
+- Enable KVM on the runner
+- Install and initialise LXD
+- Install and configure `ghvmctl`
+
+## Usage
+
+```yaml
+jobs:
+ test-snap:
+ runs-on: ubuntu-latest
+ steps:
+ - name: Setup ghvmctl
+ uses: snapcrafters/ci/setup-ghvmctl@main
+
+ - name: Prepare test environment
+ run: |
+ # Prepare the VM, install and launch the app on the desktop
+ ghvmctl prepare
+ ghvmctl install-snap signal-desktop --channel candidate
+ ghvmctl run-snap signal-desktop
+
+ - name: Take screenshots & output logs
+ run: |
+ ghvmctl screenshot-full
+ ghvmctl screenshot-window
+ ghvmctl exec "cat /home/ubuntu/signal-desktop.log"
+
+ - name: Upload screenshots
+ uses: actions/upload-artifact@v3.1.3
+ with:
+ name: "screenshots"
+ path: "~/ghvmctl-screenshots/*.png"
+```
+
+## API
+
+### Inputs
+
+None
+
+### Outputs
+
+None
diff --git a/setup-ghvmctl/action.yaml b/setup-ghvmctl/action.yaml
new file mode 100644
index 0000000..1bd7ecc
--- /dev/null
+++ b/setup-ghvmctl/action.yaml
@@ -0,0 +1,25 @@
+name: Setup ghvmctl
+description: Configure a runner for access to the KVM socket and install ghvmctl
+author: snapcrafters
+branding:
+ icon: refresh-cw
+ color: orange
+
+runs:
+ using: composite
+ steps:
+ - name: Enable KVM on the Github Actions runner
+ shell: bash
+ run: |
+ echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
+ sudo udevadm control --reload-rules
+ sudo udevadm trigger --name-match=kvm
+
+ - name: Setup LXD
+ uses: canonical/setup-lxd@v0.1.1
+
+ - name: Setup ghvmctl
+ shell: bash
+ run: |
+ sudo snap install ghvmctl
+ sudo snap connect ghvmctl:lxd lxd:lxd
diff --git a/sync-version/README.md b/sync-version/README.md
new file mode 100644
index 0000000..7a33c4a
--- /dev/null
+++ b/sync-version/README.md
@@ -0,0 +1,38 @@
+# snapcrafters/ci/sync-version
+
+Takes an `update-script` input which should be a script that automatically checks for version
+updates to the upstream application, and modifies the `snapcraft.yaml` as appropriate. This action
+takes care of identifying and committing those changes.
+
+## Usage
+
+```yaml
+jobs:
+ sync:
+ name: ๐ Sync version with upstream
+ runs-on: ubuntu-latest
+ steps:
+ - name: ๐ Sync version with upstream
+ uses: snapcrafters/ci/sync-version@main
+ with:
+ token: ${{ secrets.TOKEN }}
+ update-script: |
+ VERSION=$(
+ curl -sL https://api.github.com/repos/jnsgruk/gosherve/releases |
+ jq . | grep tag_name | grep -v beta | head -n 1 | cut -d'"' -f4 | tr -d 'v'
+ )
+ sed -i 's/^\(version: \).*$/\1'"$VERSION"'/' snap/snapcraft.yaml
+```
+
+## API
+
+### Inputs
+
+| Key | Description | Required |
+| --------------- | -------------------------------------------------------------------------------------------------- | :------: |
+| `token` | A token with permissions to commit to the repository. | Y |
+| `update-script` | A script that checks for version updates and updates `snapcraft.yaml` and other files if required. | Y |
+
+### Outputs
+
+None
diff --git a/sync-version/action.yaml b/sync-version/action.yaml
new file mode 100644
index 0000000..0a31b6a
--- /dev/null
+++ b/sync-version/action.yaml
@@ -0,0 +1,46 @@
+name: Setup ghvmctl
+description: Configure a runner for access to the KVM socket and install ghvmctl
+author: Snapcrafters
+branding:
+ icon: refresh-cw
+ color: orange
+
+inputs:
+ update-script:
+ description: "Bash script that fetches the latest version and updates the source tree as required."
+ required: true
+ token:
+ required: true
+ description: A token with write privileges to the repository.
+
+runs:
+ using: composite
+ steps:
+ - name: Checkout the source
+ uses: actions/checkout@v4
+ with:
+ token: ${{ inputs.token }}
+
+ - name: Run update script
+ shell: bash
+ run: |
+ ${{ inputs.update-script }}
+
+ - name: Check for modified files
+ shell: bash
+ id: git-check
+ run: |
+ MODIFIED=$([ -z "`git status --porcelain`" ] && echo "false" || echo "true")
+ echo "modified=$MODIFIED" >> $GITHUB_OUTPUT
+
+ - name: Commit changes
+ if: steps.git-check.outputs.modified == 'true'
+ shell: bash
+ env:
+ SNAP_NAME: ${{ github.event.repository.name }}
+ run: |
+ version="$(cat snap/snapcraft.yaml | yq -r '.version')"
+ git config --global user.name 'Snapcrafters Bot'
+ git config --global user.email 'merlijn.sebrechts+snapcrafters-bot@gmail.com'
+ git commit -am "chore: bump ${SNAP_NAME} to version $version"
+ git push
diff --git a/test-snap-build/README.md b/test-snap-build/README.md
new file mode 100644
index 0000000..811d79a
--- /dev/null
+++ b/test-snap-build/README.md
@@ -0,0 +1,47 @@
+# snapcrafters/ci/test-snap-build
+
+Designed to be a quick "smoke test" that doesn't require any special credentials or secrets. This
+action tries to build the snap "locally" on the Github Actions runner for `amd64` only. Once the
+snap is built, it is reviewed using [review-tools].
+
+Information about your snap will be automatically parsed for the review stage. If you need to
+specify plug or slot declarations per the [snapcraft-review-tools] README, you can include any of
+the following files in your repository, which will be passed to the review action:
+
+- `slot-declaration.json`
+- `.github/slot-declaration.json`
+- `plug-declaration.json`
+- `.github/plug-declaration.json`
+
+> [!NOTE]
+> We don't use `remote-build` here, because that requires access to a Launchpad token.
+> Exposing tokens in a PR build can be [dangerous] from a security standpoint.
+
+## Usage
+
+```yaml
+# ...
+jobs:
+ build:
+ name: ๐งช Build snap on amd64
+ runs-on: ubuntu-latest
+ steps:
+ - name: ๐งช Build snap on amd64
+ uses: snapcrafters/ci/test-snap-build@main
+```
+
+## API
+
+### Inputs
+
+| Key | Description | Required | Default |
+| --------- | -------------------------------------------------------------- | :------: | :------ |
+| `install` | If `true`, the built snap is install on the runner after build | N | `false` |
+
+### Outputs
+
+None
+
+[review-tools]: https://snapcraft.io/review-tools
+[snapcraft-review-tools]: https://github.com/diddlesnaps/snapcraft-review-action/tree/master
+[dangerous]: https://securitylab.github.com/research/github-actions-preventing-pwn-requests/
diff --git a/test-snap-build/action.yaml b/test-snap-build/action.yaml
new file mode 100644
index 0000000..f2ea01b
--- /dev/null
+++ b/test-snap-build/action.yaml
@@ -0,0 +1,66 @@
+name: Test Snap Build
+description: Build the snap locally using Snapcraft and review the output.
+author: Snapcrafters
+branding:
+ icon: zap
+ color: orange
+
+inputs:
+ install:
+ description: "Option to install the snap on the runner after build"
+ default: "false"
+ required: false
+
+runs:
+ using: composite
+ steps:
+ - name: Checkout the source
+ uses: actions/checkout@v4
+
+ - name: Build snap
+ uses: snapcore/action-build@v1
+ id: build
+
+ - name: Parse snap review information
+ id: parse
+ shell: bash
+ run : |
+ # Populate defaults
+ echo "classic=false" >> "$GITHUB_OUTPUT"
+ echo "slots=''" >> "$GITHUB_OUTPUT"
+ echo "plugs=''" >> "$GITHUB_OUTPUT"
+
+ # Check for classic confinement and update the output if the snap is classic
+ if [[ "$(cat snap/snapcraft.yaml | yq -r '.confinement')" == "classic" ]]; then
+ echo "classic=true" >> "$GITHUB_OUTPUT"
+ fi
+
+ # Declare the common locations for plugs/slots declarations
+ plugs_files=("plug-declaration.json" ".github/plug-declaration.json")
+ slots_files=("slot-declaration.json" ".github/slot-declaration.json")
+
+ for file in "${plugs_files[@]}"; do
+ if [[ -f "$file" ]]; then
+ echo "plugs=$file" >> "$GITHUB_OUTPUT"
+ fi
+ done
+
+ for file in "${slots_files[@]}"; do
+ if [[ -f "$file" ]]; then
+ echo "slots=$file" >> "$GITHUB_OUTPUT"
+ fi
+ done
+
+ - name: Review the built snap
+ uses: diddlesnaps/snapcraft-review-action@v1
+ with:
+ snap: ${{ steps.build.outputs.snap }}
+ isClassic: ${{ steps.parse.outputs.classic }}
+ plugs: ${{ steps.parse.outputs.plugs }}
+ slots: ${{ steps.parse.outputs.slots }}
+
+ - name: Install the snap
+ if: ${{ inputs.install == 'true' }}
+ shell: bash
+ run: |
+ sudo snap install --classic --dangerous ${{ steps.build.outputs.snap }}