Skip to content
This repository has been archived by the owner on Dec 11, 2024. It is now read-only.

Commit

Permalink
Remove the need to specify a Confluence Space key (#57)
Browse files Browse the repository at this point in the history
* Remove the need to specify a Confluence Space key

Prior to this commit users of the plugin always had to specify the key
of the Confluence Space that they wanted to be published to (and to be
created if necessary).  This commit makes the `CONFLUENCE_SPACE_KEY`
Gauge configuration variable optional rather than mandatory.

If the `CONFLUENCE_SPACE_KEY` is not provided, the plugin will derive
the Space key to be used based on the remote Git repository URL. This
convention ensures that each Git repository has its own unique
Confluence space key derived from it, i.e. a one to one mapping between
each Git repository and its associated one to one space.

The recommended way to run the plugin now is not to provide the
`CONFLUENCE_SPACE_KEY` variable, and instead to rely on the plugin to
set it.  This is particularly useful in CI/CD for instance, as it
removes the need to set the Space key manually before being able to run
the plugin.

One use case for setting the `CONFLUENCE_SPACE_KEY` is if for whatever
reason you are unable to specify a Confluence user who has permission
to create Confluence Spaces.  By setting the `CONFLUENCE_SPACE_KEY` to
be an existing Space which someone (e.g. a Confluence admin) has
created for you, you will still be able to run the plugin even without
Confluence create space permissions.

* Refactor tests so git remote url can be specified

* Bump plugin minor version
  • Loading branch information
johnboyes authored Sep 14, 2021
1 parent 1dbe8c5 commit fb5e49a
Show file tree
Hide file tree
Showing 18 changed files with 211 additions and 74 deletions.
34 changes: 19 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,49 +84,53 @@ If you find a problem with a particular version of Confluence, please

### Plugin setup

There are four mandatory variables and two optional variables to configure, as either:
There are three mandatory variables and three optional variables to configure, as either:

1. environment variables

2. properties in a
[properties file](https://docs.gauge.org/configuration.html#local-configuration-of-gauge-default-properties),
e.g. `<project_root>/env/default/anythingyoulike.properties`

The four mandatory variables to configure are:
The three mandatory variables to configure are:

`CONFLUENCE_BASE_URL` e.g. `https://example.com/path-to-your-confluence-wiki` for Confluence Server, or `https://example.atlassian.net` for Confluence Cloud

`CONFLUENCE_USERNAME`

`CONFLUENCE_TOKEN`
`CONFLUENCE_TOKEN` (This can either be a token or the password for the given Confluence username)

`CONFLUENCE_SPACE_KEY`

Use a dedicated, empty [Confluence Space](https://support.atlassian.com/confluence-cloud/docs/use-spaces-to-organize-your-work/)
that will contain just the Gauge specifications and nothing else.

NB You can use [Confluence's include macro](https://confluence.atlassian.com/doc/include-page-macro-139514.html)
to include the [page tree](https://confluence.atlassian.com/conf59/page-tree-macro-792499177.html) of Gauge Specs
(that gets created by this plugin) in as many of your existing spaces as you like.

The two optional variables to configure are:
The three optional variables to configure are:

`GAUGE_LOG_LEVEL`

`DRY_RUN`

`CONFLUENCE_SPACE_KEY`

___
The `GAUGE_LOG_LEVEL` variable can be set to `debug` or `info` (default is `info`).
It controls the logging level both for the log files which are generated, _and_ what is logged to the console.
NB the command line flag `--log-level` does not have any effect on the logging for this plugin.


___
**Setting the `DRY_RUN` variable to `true` means that running the plugin does not publish specs to Confluence.**

Instead the plugin just checks that the specs are in a valid publishable state (e.g. that there are no duplicate
spec headings).
This is very useful e.g. **in a CI/CD pipeline the plugin can run in dry run mode on feature branches and pull
requests.** This ensures that the Gauge specs are always in good shape to be automatically published by the CI/CD pipeline upon any push to the trunk branch (e.g. upon a successful pull request merge).

___
If the `CONFLUENCE_SPACE_KEY` is not provided, the plugin will derive the Space key to be used based on the remote Git repository URL. This convention ensures that each Git repository has its own unique Confluence space key derived from it, i.e. a one to one mapping between each Git repository and its associated one to one space.

The recommended way to run the plugin is not to provide the `CONFLUENCE_SPACE_KEY` variable, and instead to rely on the plugin to set it. This is particularly useful in CI/CD for instance, as it removes the need to set the Space key manually before being able to run the plugin.

One use case for setting the `CONFLUENCE_SPACE_KEY` is if for whatever reason you are unable to specify a Confluence user who has permission to create Confluence Spaces. By setting the `CONFLUENCE_SPACE_KEY` to be an existing Space which someone (e.g. a Confluence admin) has created for you, you will still be able to run the plugin even without Confluence create space permissions. In this case use a dedicated, empty [Confluence Space](https://support.atlassian.com/confluence-cloud/docs/use-spaces-to-organize-your-work/) that will contain just the Gauge specifications and nothing else.

NB You can use [Confluence's include macro](https://confluence.atlassian.com/doc/include-page-macro-139514.html)
to include the [page tree](https://confluence.atlassian.com/conf59/page-tree-macro-792499177.html) of Gauge Specs
(that gets created by this plugin) in as many of your existing spaces as you like.
___

### Running the plugin (i.e. publishing specs to Confluence)

Expand Down
1 change: 0 additions & 1 deletion functional-tests/specs/check_config_vars.spec
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ Tags: create-space-manually
|CONFLUENCE_BASE_URL |
|CONFLUENCE_USERNAME |
|CONFLUENCE_TOKEN |
|CONFLUENCE_SPACE_KEY|

## The plugin fails if required configuration variables are not set

Expand Down
4 changes: 0 additions & 4 deletions functional-tests/specs/dry_run.spec
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,12 @@ Tags: create-space-manually
The absence of the "create-space-manually" tag means the Confluence Space does not
exist for this scenario

* Space does not exist

* Activate dry run mode

* Publish "1" specs to Confluence

* Output contains "Dry run finished successfully"

* Space does not exist


__________________________________________________________________________________________

Expand Down
6 changes: 4 additions & 2 deletions functional-tests/specs/space_creation.spec
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

## If the Space does not already exist, the plugin will create it

* Space does not exist

* Publish "1" specs to Confluence

* Specs "did" get published

* Space has key "GITHUBCOMEXAMPLEUSEREXAMPLEREPO"

* Space has name "Gauge specs for example-user/example-repo"

* Output contains "Success: published 2 specs and directory pages to Confluence Space named: Gauge specs for example-user/example-repo"

The `example-user/example-repo` comes from the [dummy Git remote URL config in the
test framework code][1]. When users run the plugin the Space name will be taken from
the Git remote URL of the Git repository that the plugin is executed on.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@
import com.thoughtworks.gauge.datastore.ScenarioDataStore;
import com.thoughtworks.gauge.test.StepImpl;
import com.thoughtworks.gauge.test.confluence.Confluence;
import com.thoughtworks.gauge.test.git.Config.GitConfig;

import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.ArrayUtils;
Expand Down Expand Up @@ -333,9 +332,9 @@ public void deleteSpec(String specName) {
getSpecFile(specName).delete();
}

public void addGitConfig(GitConfig gitConfig) throws Exception {
public void addGitConfig(String remoteOriginURL) throws Exception {
executeGitCommand("init");
executeGitCommand("remote", "add", "origin", gitConfig.remoteOriginURL());
executeGitCommand("remote", "add", "origin", remoteOriginURL);
}

public void simulateGitDetachedHead() throws IOException {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,15 @@
import com.thoughtworks.gauge.test.common.GaugeProject;
import com.thoughtworks.gauge.test.common.Util;
import com.thoughtworks.gauge.test.git.Config.GitConfig;
import static com.thoughtworks.gauge.test.confluence.Confluence.getGitRemoteURLFromScenarioDataStore;

public class ProjectBuilder {

private String language;
private String projName;
private boolean deleteExampleSpec;
private boolean remoteTemplate;
private boolean gitConfig;
private boolean addGitConfig;

public ProjectBuilder() {
this.remoteTemplate = false;
Expand All @@ -28,12 +29,12 @@ public ProjectBuilder withProjectName(String projName) {
}

public ProjectBuilder withGitConfig() {
this.gitConfig = true;
this.addGitConfig = true;
return this;
}

public ProjectBuilder withoutGitConfig() {
this.gitConfig = false;
this.addGitConfig = false;
return this;
}

Expand All @@ -49,8 +50,13 @@ public GaugeProject build(boolean expectFailure) throws Exception {
+ currentProject.getLastProcessStderr() + "\n\nSTDOUT:\n\n"
+ currentProject.getLastProcessStdout());

if (this.gitConfig)
currentProject.addGitConfig(GitConfig.HTTPS);
if (this.addGitConfig) {
if (getGitRemoteURLFromScenarioDataStore() != null) {
currentProject.addGitConfig(getGitRemoteURLFromScenarioDataStore());
} else {
currentProject.addGitConfig(GitConfig.HTTPS.remoteOriginURL());
}
}

if (this.deleteExampleSpec)
currentProject.deleteSpec(Util.combinePath("specs", "example"));
Expand Down
Original file line number Diff line number Diff line change
@@ -1,20 +1,22 @@
package com.thoughtworks.gauge.test.confluence;

import com.thoughtworks.gauge.BeforeScenario;
import com.thoughtworks.gauge.Step;
import com.thoughtworks.gauge.Table;
import com.thoughtworks.gauge.TableRow;
import com.thoughtworks.gauge.AfterScenario;
import com.thoughtworks.gauge.datastore.ScenarioDataStore;
import com.thoughtworks.gauge.test.implementation.Console;

import static com.thoughtworks.gauge.test.confluence.ConfluenceClient.deleteSpace;
import static com.thoughtworks.gauge.test.confluence.ConfluenceClient.doesSpaceExist;
import static org.assertj.core.api.Assertions.assertThat;

import java.io.IOException;
import java.time.LocalTime;
import java.util.concurrent.TimeUnit;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

import com.thoughtworks.gauge.AfterScenario;
import com.thoughtworks.gauge.BeforeScenario;
import com.thoughtworks.gauge.Step;
import com.thoughtworks.gauge.Table;
import com.thoughtworks.gauge.TableRow;
import com.thoughtworks.gauge.datastore.ScenarioDataStore;
import com.thoughtworks.gauge.test.implementation.Console;

public class Confluence {

Expand All @@ -24,6 +26,7 @@ public class Confluence {
private static final String DRY_RUN_MODE = "dry-run-mode";
private static final String CONFLUENCE_USERNAME = "confluence-username";
private static final String CONFLUENCE_TOKEN = "confluence-token";
private static final String GIT_REMOTE_URL_KEY_NAME = "git-remote-url";

public static String getScenarioSpaceKey() {
return Objects.toString(ScenarioDataStore.get(SCENARIO_SPACE_KEY_NAME), "");
Expand All @@ -45,31 +48,33 @@ public static String getConfluenceTokenFromScenarioDataStore() {
return (String) ScenarioDataStore.get(CONFLUENCE_TOKEN);
}

@BeforeScenario
public void setDryRunModeOff() {
ScenarioDataStore.put(DRY_RUN_MODE, false);
public static String getGitRemoteURLFromScenarioDataStore() {
return (String) ScenarioDataStore.get(GIT_REMOTE_URL_KEY_NAME);
}

@BeforeScenario
public void setSpaceKeyName() {
ScenarioDataStore.put(SCENARIO_SPACE_KEY_NAME, generateUniqueSpaceKeyName());
public void setDryRunModeOff() {
ScenarioDataStore.put(DRY_RUN_MODE, false);
}

@BeforeScenario(tags = {"create-space-manually"})
public void beforeScenario() {
ScenarioDataStore.put(SCENARIO_SPACE_KEY_NAME, generateUniqueSpaceKeyName());
String spaceHomepageID = ConfluenceClient.createSpace(getScenarioSpaceKey(), SCENARIO_SPACE_NAME);
ScenarioDataStore.put(SCENARIO_SPACE_HOMEPAGE_ID_KEY_NAME, spaceHomepageID);
}

@AfterScenario
public void setConfluenceUsernameAndTokenFromEnvVar() {
@AfterScenario()
public void afterScenario() {
if ((!getScenarioSpaceKey().isEmpty()) && doesSpaceExist(getScenarioSpaceKey())) {
deleteSpace(getScenarioSpaceKey());
}
ScenarioDataStore.remove(CONFLUENCE_USERNAME);
ScenarioDataStore.remove(CONFLUENCE_TOKEN);
}

@AfterScenario(tags = {"create-space-manually"})
public void afterScenario() {
ConfluenceClient.deleteSpace(getScenarioSpaceKey());
public void setScenarioSpaceKeyName(String keyName) {
ScenarioDataStore.put(SCENARIO_SPACE_KEY_NAME, keyName);
}

@Step("Activate dry run mode")
Expand All @@ -94,6 +99,13 @@ public void assertSpaceHasName(String name) {
assertThat(space.getName()).isEqualTo(name);
}

@Step("Space has key <key>")
public void assertSpaceHasKey(String key) {
setScenarioSpaceKeyName(key);
Space space = new Space(getScenarioSpaceKey());
assertThat(space.getKey()).isEqualTo(key);
}

@Step("Space has description <description>")
public void assertSpaceHasDescription(String description) {
Space space = new Space(getScenarioSpaceKey());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ public Space(String key) {
this.jsonSpace = ConfluenceClient.getSpace(key);
}

public String getKey() {
return jsonSpace.getString("key");
}

public String getName() {
return jsonSpace.getString("name");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ public String remoteOriginURL() {

@Step("Add <type> Git config to project")
public void addGitConfigToProject(String gitConfig) throws Exception {
getCurrentProject().addGitConfig(GitConfig.valueOf(gitConfig));
getCurrentProject().addGitConfig(GitConfig.valueOf(gitConfig).remoteOriginURL());
}

@Step("Simulate Git detached HEAD")
Expand Down
11 changes: 8 additions & 3 deletions internal/confluence/publisher.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,9 @@ type Publisher struct {

// NewPublisher instantiates a new Publisher.
func NewPublisher(m *gauge_messages.SpecDetails) Publisher {
spaceKey := env.GetRequired("CONFLUENCE_SPACE_KEY")
apiClient := api.NewClient()

return Publisher{apiClient: apiClient, space: newSpace(spaceKey, apiClient), specs: makeSpecsMap(m),
return Publisher{apiClient: apiClient, space: newSpace(apiClient), specs: makeSpecsMap(m),
dryRunPages: make(map[string]page)}
}

Expand Down Expand Up @@ -91,7 +90,13 @@ func (p *Publisher) Publish(specPaths []string) (err error) {
return err
}

logger.Infof(true, "Success: published %d specs and directory pages to Confluence", len(p.space.publishedPages))
spaceName, err := p.space.name()
if err != nil {
return err
}

logger.Infof(true, "Success: published %d specs and directory pages to Confluence Space named: %s",
len(p.space.publishedPages), spaceName)

return nil
}
Expand Down
44 changes: 40 additions & 4 deletions internal/confluence/space.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package confluence

import (
"fmt"
"net/url"
"os"
"path/filepath"
"strings"

"github.com/agilepathway/gauge-confluence/internal/confluence/api"
"github.com/agilepathway/gauge-confluence/internal/confluence/api/http"
"github.com/agilepathway/gauge-confluence/internal/confluence/time"
"github.com/agilepathway/gauge-confluence/internal/env"
"github.com/agilepathway/gauge-confluence/internal/git"
"github.com/agilepathway/gauge-confluence/internal/logger"
str "github.com/agilepathway/gauge-confluence/internal/strings"
)

type space struct {
Expand All @@ -23,12 +27,44 @@ type space struct {
}

// newSpace initialises a new space.
func newSpace(key string, apiClient api.Client) space {
return space{key: key, publishedPages: make(map[string]page), apiClient: apiClient}
func newSpace(apiClient api.Client) space {
return space{publishedPages: make(map[string]page), apiClient: apiClient}
}

func (s *space) setup() error {
err := s.createIfDoesNotAlreadyExist()
func retrieveOrGenerateKey() (string, error) {
retrievedKey := os.Getenv("CONFLUENCE_SPACE_KEY")
if retrievedKey != "" {
return retrievedKey, nil
}

return generateKey()
}

func generateKey() (string, error) {
gitWebURL, err := git.WebURL()
if err != nil {
return "", err
}

return keyFmt(gitWebURL), nil
}

func keyFmt(u *url.URL) string {
hostAndPath := u.Host + u.Path
alphanumeric := str.StripNonAlphaNumeric(hostAndPath)

return strings.ToUpper(alphanumeric)
}

func (s *space) setup() error { // nolint:funlen
key, err := retrieveOrGenerateKey()
if err != nil {
return err
}

s.key = key

err = s.createIfDoesNotAlreadyExist()
if err != nil {
return err
}
Expand Down
Loading

0 comments on commit fb5e49a

Please sign in to comment.