Skip to content

Commit 10f6bcb

Browse files
authored
Inherit configuration from other buildpacks (#4)
* Support for inheriting Release Phase configuration from other buildpacks via Build Plan * Improve docs
1 parent 21660f8 commit 10f6bcb

File tree

7 files changed

+375
-92
lines changed

7 files changed

+375
-92
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

buildpacks/release-phase/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ indoc = "2"
1414
release_artifacts = { path = "../../common/release_artifacts" }
1515
release_commands = { path = "../../common/release_commands" }
1616
tokio = { version = "1.40.0", features = ["macros", "rt-multi-thread"] }
17+
toml = { version = "0.8", features = ["preserve_order"] }
1718

1819
[dev-dependencies]
1920
libcnb-test = "=0.22.0"

buildpacks/release-phase/README.md

Lines changed: 62 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -14,18 +14,10 @@ schema-version = "0.2"
1414
uri = "heroku/release-phase"
1515
```
1616

17-
### Release Build command
18-
19-
```toml
20-
[com.heroku.phase.release-build]
21-
command = "bash"
22-
args = ["-c", "npm build"]
23-
```
24-
25-
This command must output release artifacts into `/workspace/static-artifacts/`. The content of this directory will be stored during Release Phase by the `RELEASE_ID`, and then automatically retrieved for `web` processes, during start-up.
26-
2717
### Release commands
2818

19+
*Multiple `release` commands are supported as a TOML array, their entries declared by `[[…]]`.*
20+
2921
```toml
3022
[[com.heroku.phase.release]]
3123
command = "bash"
@@ -38,6 +30,18 @@ args = ["-c", "./bin/purge-cache"]
3830

3931
These commands are ephemeral. No changes to the filesystem are persisted.
4032

33+
### Release Build command
34+
35+
*Only a single `release-build` command is supported. The entry must be declared with `[…]`.*
36+
37+
```toml
38+
[com.heroku.phase.release-build]
39+
command = "bash"
40+
args = ["-c", "npm build"]
41+
```
42+
43+
This command must output release artifacts into `/workspace/static-artifacts/`. The content of this directory will be stored during Release Phase by the `RELEASE_ID`, and then automatically retrieved for `web` processes, during start-up.
44+
4145
## Configuration: runtime environment vars
4246

4347
### `RELEASE_ID`
@@ -66,4 +70,52 @@ Artifacts are stored at the `STATIC_ARTIFACTS_URL` with the name `release-<RELEA
6670

6771
**Required for `s3` URLs.** The access secret.
6872

73+
## Inherited Configuration
74+
75+
Other buildpacks can return a [Build Plan](https://github.com/buildpacks/spec/blob/main/buildpack.md#build-plan-toml) from `detect` for Release Phase configuration.
76+
77+
The array of `release` commands defined in an app's `project.toml` and the inherited Build Plan are combined into a sequence:
78+
1. `release` commands inherited from the Build Plan
79+
2. `release` commands declared in `project.toml`.
80+
81+
Only a single `release-build` command will be executed during Release Phase:
82+
* the `release-build` command declared in `project.toml` takes precedence
83+
* otherwise `release-build` inherited from Build Plan
84+
* if multiple Build Plan entries declare `release-build`, the last one takes precedence.
85+
86+
This example sets a `release` & `release-build` commands in the build plan, using the supported [project configuration](#configuration-projecttoml):
6987

88+
```toml
89+
[[requires]]
90+
name = "release-phase"
91+
92+
[requires.metadata.release-build]
93+
command = "bash"
94+
args = ["-c", "npm run build"]
95+
source = "My Awesome Buildpack"
96+
97+
[[requires.metadata.release]]
98+
command = "bash"
99+
args = ["-c", "echo 'Hello world!'"]
100+
source = "My Awesome Buildpack"
101+
```
102+
103+
Example using [libcnb.rs](https://github.com/heroku/libcnb.rs):
104+
105+
```rust
106+
fn detect(&self, context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
107+
let mut release_phase_req = Require::new("release-phase");
108+
let _ = release_phase_req.metadata(toml! {
109+
[release-build]
110+
command = "bash"
111+
args = ["-c", "npm run build"]
112+
source = "My Awesome Buildpack"
113+
});
114+
let plan_builder = BuildPlanBuilder::new()
115+
.requires(release_phase_req);
116+
117+
DetectResultBuilder::pass()
118+
.build_plan(plan_builder.build())
119+
.build()
120+
}
121+
```

buildpacks/release-phase/src/errors.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ use commons_ruby::output::build_log::{BuildLog, Logger, StartedLogger};
33
use commons_ruby::output::fmt;
44
use commons_ruby::output::fmt::DEBUG_INFO;
55
use indoc::formatdoc;
6+
use libcnb::TomlFileError;
67
use std::fmt::Display;
78
use std::io::stdout;
89

@@ -16,6 +17,7 @@ pub(crate) enum ReleasePhaseBuildpackError {
1617
CannotInstallArtifactLoader(std::io::Error),
1718
CannotInstallCommandExecutor(std::io::Error),
1819
CannotCreatWebExecD(std::io::Error),
20+
CannotReadProjectToml(TomlFileError),
1921
ConfigurationFailed(release_commands::Error),
2022
}
2123

@@ -59,6 +61,13 @@ fn on_buildpack_error(error: ReleasePhaseBuildpackError, logger: Box<dyn Started
5961
Cannot create exec.d/web for {buildpack_name}
6062
", buildpack_name = fmt::value(BUILDPACK_NAME) });
6163
}
64+
ReleasePhaseBuildpackError::CannotReadProjectToml(error) => {
65+
print_error_details(logger, &error)
66+
.announce()
67+
.error(&formatdoc! {"
68+
Error reading project.toml for {buildpack_name}
69+
", buildpack_name = fmt::value(BUILDPACK_NAME) });
70+
}
6271
ReleasePhaseBuildpackError::ConfigurationFailed(error) => {
6372
print_error_details(logger, &error)
6473
.announce()

buildpacks/release-phase/src/main.rs

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ mod setup_release_phase;
33

44
use crate::errors::{on_error, ReleasePhaseBuildpackError};
55
use libcnb::build::{BuildContext, BuildResult, BuildResultBuilder};
6+
use libcnb::data::build_plan::{BuildPlanBuilder, Require};
67
use libcnb::data::launch::{LaunchBuilder, ProcessBuilder};
78
use libcnb::data::process_type;
89
use libcnb::detect::{DetectContext, DetectResult, DetectResultBuilder};
@@ -33,7 +34,13 @@ impl Buildpack for ReleasePhaseBuildpack {
3334
type Error = ReleasePhaseBuildpackError;
3435

3536
fn detect(&self, _context: DetectContext<Self>) -> libcnb::Result<DetectResult, Self::Error> {
36-
DetectResultBuilder::pass().build()
37+
let plan_builder = BuildPlanBuilder::new()
38+
.provides("release-phase")
39+
.requires(Require::new("release-phase"));
40+
41+
DetectResultBuilder::pass()
42+
.build_plan(plan_builder.build())
43+
.build()
3744
}
3845

3946
fn build(&self, context: BuildContext<Self>) -> libcnb::Result<BuildResult, Self::Error> {

buildpacks/release-phase/src/setup_release_phase.rs

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,38 @@
11
use std::fs;
22

33
use crate::{ReleasePhaseBuildpack, ReleasePhaseBuildpackError};
4-
use libcnb::additional_buildpack_binary_path;
54
use libcnb::data::layer_name;
65
use libcnb::layer::LayerRef;
6+
use libcnb::{additional_buildpack_binary_path, read_toml_file};
77
use libcnb::{build::BuildContext, layer::UncachedLayerDefinition};
88
use libherokubuildpack::log::log_info;
99
use release_commands::{generate_commands_config, write_commands_config};
10+
use toml::Table;
1011

1112
pub(crate) fn setup_release_phase(
1213
context: &BuildContext<ReleasePhaseBuildpack>,
1314
) -> Result<
1415
Option<LayerRef<ReleasePhaseBuildpack, (), ()>>,
1516
libcnb::Error<ReleasePhaseBuildpackError>,
1617
> {
17-
let commands_config = generate_commands_config(&context.app_dir.join("project.toml"))
18+
let project_toml_path = &context.app_dir.join("project.toml");
19+
let project_toml = if project_toml_path.is_file() {
20+
read_toml_file::<toml::Value>(project_toml_path)
21+
.map_err(ReleasePhaseBuildpackError::CannotReadProjectToml)?
22+
} else {
23+
toml::Table::new().into()
24+
};
25+
26+
// Load a table of Build Plan [requires.metadata] from context.
27+
// When a key is defined multiple times, the last one wins.
28+
let mut build_plan_config = Table::new();
29+
context.buildpack_plan.entries.iter().for_each(|e| {
30+
e.metadata.iter().for_each(|(k, v)| {
31+
build_plan_config.insert(k.to_owned(), v.to_owned());
32+
});
33+
});
34+
35+
let commands_config = generate_commands_config(&project_toml, build_plan_config)
1836
.map_err(ReleasePhaseBuildpackError::ConfigurationFailed)?;
1937

2038
if commands_config.release.is_none() && commands_config.release_build.is_none() {

0 commit comments

Comments
 (0)