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
4 changes: 4 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
*
!docker/
docker/*
!docker/run-tests.sh
61 changes: 61 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
name: CI

on:
push:
pull_request:

jobs:
syntax:
runs-on: ubuntu-latest
env:
ANSIBLE_ROLES_PATH: ${{ github.workspace }}/.ansible/roles
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Ansible
run: |
python -m pip install --upgrade pip
python -m pip install ansible-core
ansible-galaxy collection install ansible.posix
mkdir -p "$ANSIBLE_ROLES_PATH"
ansible-galaxy role install -f -p "$ANSIBLE_ROLES_PATH" ansistrano.deploy
ln -sfn "$GITHUB_WORKSPACE" "$ANSIBLE_ROLES_PATH/local-ansistrano"

- name: Run syntax check
run: |
printf 'localhost ansible_connection=local\n' > inventory
ansible-playbook -i inventory --syntax-check test/test.yml

integration:
runs-on: ubuntu-latest
env:
ANSIBLE_ROLES_PATH: ${{ github.workspace }}/.ansible/roles
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Ansible
run: |
python -m pip install --upgrade pip
python -m pip install ansible-core
ansible-galaxy collection install ansible.posix
mkdir -p "$ANSIBLE_ROLES_PATH"
ansible-galaxy role install -f -p "$ANSIBLE_ROLES_PATH" ansistrano.deploy
ln -sfn "$GITHUB_WORKSPACE" "$ANSIBLE_ROLES_PATH/local-ansistrano"

- name: Run integration tests
run: |
printf 'localhost ansible_connection=local\n' > inventory
ansible-playbook -i inventory --connection=local --become -e update_cache=1 -v test/deploy.yml
ansible-playbook -i inventory --connection=local --become -e update_cache=1 -v test/test.yml
30 changes: 30 additions & 0 deletions .github/workflows/galaxy-import.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
name: Refresh Ansible Galaxy

on:
push:
tags:
- "*"
workflow_dispatch:

jobs:
import:
name: Import role on tag
runs-on: ubuntu-latest

steps:
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Install Ansible
run: python -m pip install --upgrade pip ansible-core

- name: Trigger Ansible Galaxy import
env:
GALAXY_API_KEY: ${{ secrets.GALAXY_API_KEY }}
run: >
ansible-galaxy role import
--api-key "$GALAXY_API_KEY"
ansistrano
rollback
30 changes: 0 additions & 30 deletions .travis.yml

This file was deleted.

12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,16 @@
[![CI](https://github.com/ansistrano/rollback/actions/workflows/ci.yml/badge.svg)](https://github.com/ansistrano/rollback/actions/workflows/ci.yml)

Further details on Ansistrano
-----------------------------

If you want to know more about Ansistrano, please check the [complete Ansistrano docs](https://github.com/ansistrano/deploy/blob/master/README.md)

Testing
-------

The role is tested in GitHub Actions on `ubuntu-latest` with a modern `ansible-core` setup. For local development, use the Docker runner:

```bash
./scripts/test-in-docker.sh syntax
./scripts/test-in-docker.sh integration
```
31 changes: 31 additions & 0 deletions docker/run-tests.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#!/usr/bin/env bash
set -euo pipefail

mode="${1:-integration}"
shift || true

repo_dir="/workspace/rollback"
inventory_file="/tmp/ansistrano-inventory"
roles_path="/etc/ansible/roles"

mkdir -p "${roles_path}"
ln -sfn "${repo_dir}" "${roles_path}/local-ansistrano"
printf 'localhost ansible_connection=local\n' > "${inventory_file}"

ANSIBLE_ROLES_PATH="${roles_path}" ansible-galaxy role install -f -p "${roles_path}" ansistrano.deploy

cd "${repo_dir}/test"

case "${mode}" in
syntax)
exec env ANSIBLE_ROLES_PATH="${roles_path}" ansible-playbook -i "${inventory_file}" --syntax-check test.yml "$@"
;;
integration)
env ANSIBLE_ROLES_PATH="${roles_path}" ansible-playbook -i "${inventory_file}" --connection=local --become -e update_cache=1 -v deploy.yml "$@"
exec env ANSIBLE_ROLES_PATH="${roles_path}" ansible-playbook -i "${inventory_file}" --connection=local --become -e update_cache=1 -v test.yml "$@"
;;
*)
echo "Usage: run-ansistrano-tests [syntax|integration] [extra ansible-playbook args...]" >&2
exit 2
;;
esac
17 changes: 17 additions & 0 deletions docker/test-runner.Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
FROM python:3.12-slim-bookworm

RUN apt-get update \
&& apt-get install -y --no-install-recommends \
bash \
ca-certificates \
rsync \
sudo \
&& python -m pip install --upgrade pip \
&& python -m pip install --no-cache-dir ansible-core \
&& ansible-galaxy collection install ansible.posix \
&& rm -rf /var/lib/apt/lists/*

COPY --chmod=755 docker/run-tests.sh /usr/local/bin/run-ansistrano-tests

WORKDIR /workspace/rollback
ENTRYPOINT ["run-ansistrano-tests"]
25 changes: 25 additions & 0 deletions scripts/test-in-docker.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
#!/usr/bin/env bash
set -euo pipefail

script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
repo_dir="$(cd "${script_dir}/.." && pwd)"

mode="${1:-integration}"
shift || true

image_name="${ANSISTRANO_TEST_IMAGE:-ansistrano-rollback-test}"

docker build -f "${repo_dir}/docker/test-runner.Dockerfile" -t "${image_name}" "${repo_dir}"

docker_run_args=(
run
--rm
-e ANSIBLE_VERSION=docker
-v "${repo_dir}:/workspace/rollback"
)

if [ -t 0 ] && [ -t 1 ]; then
docker_run_args+=(-it)
fi

docker "${docker_run_args[@]}" "${image_name}" "${mode}" "$@"
2 changes: 1 addition & 1 deletion tasks/cleanup.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,5 @@
- name: ANSISTRANO | Remove rolled back version
file:
state: absent
path: "{{ ansistrano_releases_path.stdout }}/{{ ansistrano_current_release_version.stdout }}"
path: "{{ ansistrano_releases_path }}/{{ ansistrano_current_release_version }}"
when: ansistrano_remove_rolled_back
37 changes: 21 additions & 16 deletions tasks/setup.yml
Original file line number Diff line number Diff line change
@@ -1,24 +1,30 @@
---
- name: ANSISTRANO | Get releases path
command: echo "{{ ansistrano_deploy_to }}/{{ ansistrano_version_dir }}"
check_mode: no
register: ansistrano_releases_path
set_fact:
ansistrano_releases_path: "{{ ansistrano_deploy_to }}/{{ ansistrano_version_dir }}"

- name: ANSISTRANO | Get number of releases
shell: echo `ls -1t {{ ansistrano_releases_path.stdout }} | wc -l`
register: ansistrano_versions_count
- name: ANSISTRANO | Gather releases
find:
paths: "{{ ansistrano_releases_path }}"
file_type: directory
recurse: false
register: ansistrano_releases

- name: ANSISTRANO | Check if there is more than one release
fail:
msg: "Could not roll back the code because there is no prior release"
when: ansistrano_versions_count.stdout|int <= 1
when: ansistrano_releases.matched | int <= 1

- name: ANSISTRANO | Sort releases by mtime
set_fact:
ansistrano_sorted_releases: "{{ ansistrano_releases.files | sort(attribute='mtime', reverse=true) }}"

- name: ANSISTRANO | Get current release version
shell: echo `ls -1t {{ ansistrano_releases_path.stdout }} | head -n 1`
register: ansistrano_current_release_version
set_fact:
ansistrano_current_release_version: "{{ ansistrano_sorted_releases[0].path | basename }}"

- stat:
path: "{{ ansistrano_releases_path.stdout }}/{{ ansistrano_rollback_to_release }}"
path: "{{ ansistrano_releases_path }}/{{ ansistrano_rollback_to_release }}"
register: stat_rollback_release_version
when: ansistrano_rollback_to_release != ""

Expand All @@ -28,14 +34,13 @@
when: ansistrano_rollback_to_release != "" and (stat_rollback_release_version.stat.exists is not defined or stat_rollback_release_version.stat.isdir == False)

- name: ANSISTRANO | Get previous releases version
shell: echo `ls -1t {{ ansistrano_releases_path.stdout }} | head -n 2 | tail -n 1`
register: ansistrano_previous_release_version
set_fact:
ansistrano_previous_release_version: "{{ ansistrano_sorted_releases[1].path | basename }}"

- name: ANSISTRANO | Get rollback release version
set_fact:
ansistrano_rollback_release_version: "{{ ansistrano_rollback_to_release if ansistrano_rollback_to_release != '' else ansistrano_previous_release_version.stdout }}"
ansistrano_rollback_release_version: "{{ ansistrano_rollback_to_release if ansistrano_rollback_to_release != '' else ansistrano_previous_release_version }}"

- name: ANSISTRANO | Get release path
command: echo "{{ ansistrano_releases_path.stdout }}/{{ ansistrano_rollback_release_version }}"
check_mode: no
register: ansistrano_release_path
set_fact:
ansistrano_release_path: "{{ ansistrano_releases_path }}/{{ ansistrano_rollback_release_version }}"
43 changes: 43 additions & 0 deletions test/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,48 @@
hosts: all
vars:
ansistrano_deploy_to: "/tmp/my-app.com"
ansistrano_release_version: "20000101000000Z"
roles:
- { role: ansistrano.deploy }

- name: Do a second deploy to create a rollback target
hosts: all
vars:
ansistrano_deploy_to: "/tmp/my-app.com"
ansistrano_release_version: "20000101000001Z"
roles:
- { role: ansistrano.deploy }

- name: Do multiple deploys to support explicit rollback tests
hosts: all
vars:
ansistrano_deploy_to: "/tmp/my-other-app.com"
tasks:
- name: Clear previous deploy target
file:
path: "{{ ansistrano_deploy_to }}"
state: absent

- name: Deploy first release for explicit rollback tests
hosts: all
vars:
ansistrano_deploy_to: "/tmp/my-other-app.com"
ansistrano_release_version: "20000101000000Z"
roles:
- { role: ansistrano.deploy }

- name: Deploy second release for explicit rollback tests
hosts: all
vars:
ansistrano_deploy_to: "/tmp/my-other-app.com"
ansistrano_release_version: "20000101000001Z"
roles:
- { role: ansistrano.deploy }

- name: Deploy third release for explicit rollback tests
hosts: all
vars:
ansistrano_deploy_to: "/tmp/my-other-app.com"
ansistrano_release_version: "20000101000002Z"
roles:
- { role: ansistrano.deploy }
55 changes: 55 additions & 0 deletions test/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,58 @@
ansistrano_deploy_to: "/tmp/my-app.com"
roles:
- { role: local-ansistrano }
post_tasks:
- name: Assert current points to the previous release
stat:
path: "{{ ansistrano_deploy_to }}/current"
register: current_release

- name: Assert previous release remains available
stat:
path: "{{ ansistrano_deploy_to }}/releases/20000101000000Z"
register: previous_release

- name: Assert rolled back release was removed
stat:
path: "{{ ansistrano_deploy_to }}/releases/20000101000001Z"
register: removed_release

- name: Assert default rollback result
assert:
that:
- current_release.stat.islnk is defined and current_release.stat.islnk
- current_release.stat.lnk_target == "./releases/20000101000000Z"
- previous_release.stat.exists is defined and previous_release.stat.exists
- removed_release.stat.exists is defined and not removed_release.stat.exists

- name: Rolling back to a specific release without deleting the current one
hosts: all
vars:
ansistrano_deploy_to: "/tmp/my-other-app.com"
ansistrano_remove_rolled_back: false
ansistrano_rollback_to_release: "20000101000000Z"
roles:
- { role: local-ansistrano }
post_tasks:
- name: Assert current points to the requested release
stat:
path: "{{ ansistrano_deploy_to }}/current"
register: current_release

- name: Assert requested rollback release still exists
stat:
path: "{{ ansistrano_deploy_to }}/releases/20000101000000Z"
register: requested_release

- name: Assert rolled back release is still present
stat:
path: "{{ ansistrano_deploy_to }}/releases/20000101000002Z"
register: kept_release

- name: Assert explicit rollback result
assert:
that:
- current_release.stat.islnk is defined and current_release.stat.islnk
- current_release.stat.lnk_target == "./releases/20000101000000Z"
- requested_release.stat.exists is defined and requested_release.stat.exists
- kept_release.stat.exists is defined and kept_release.stat.exists
Loading